New Feature Pagination (#4704)

* common pagination component

* Pagination for Doc Store Dashboard

* Pagination for Executions Dashboard

* Pagination Support for Tables

* lint fixes

* update view message dialog UI

* initial loading was ignoring the pagination counts

* 1) default page size change
2) ensure page limits are passed on load
3) co-pilot review comments (n+1 query)
4)

* 1) default page size change
2) ensure page limits are passed on load
3) co-pilot review comments (n+1 query)
4) refresh lists after insert/delete.

* Enhancement: Improve handling of empty responses in DocumentStore and API key services

- Added check for empty entities in DocumentStoreDTO.fromEntities to return an empty array.
- Updated condition in getAllDocumentStores to handle total count correctly, allowing for zero total.
- Refined logic in getAllApiKeys to check for empty keys and ensure correct API key retrieval.
- Adjusted UI components to safely handle potential undefined apiKeys array.

* Refresh API key list on pagination change

* Enhancement: Update pagination and filter handling across components
- Increased default items per page in AgentExecutions from 10 to 12.
- Improved JSON parsing for chat type and feedback type filters in ViewMessagesDialog.
- Enhanced execution filtering logic in AgentExecutions to ensure proper pagination and state management.
- Refactored filter section in AgentExecutions for better readability and functionality.
- Updated refresh logic in Agentflows to use the correct agentflow version.

* add workspaceId to removeAllChatMessages

* Refactor chat message retrieval logic for improved efficiency and maintainability

- Introduced a new `handleFeedbackQuery` function to streamline feedback-related queries.
- Enhanced pagination handling for session-based queries in `getMessagesWithFeedback`.
- Updated `ViewMessagesDialog` to sort messages in descending order by default.
- Simplified image rendering logic in `DocumentStoreTable` for better readability.

* - Update  `validateChatflowAPIKey` and `validateAPIKey` functions to get the correct keys array
- Enhanced error handling in the `sanitizeExecution` function to ensure safe access to nested properties

* Refactor API key validation logic for improved accuracy and error handling

- Consolidated API key validation in `validateAPIKey` to return detailed validation results.
- Updated `validateFlowAPIKey` to streamline flow API key validation.
- Introduced `getApiKeyById` function in the API key service for better key retrieval.
- Removed unused function `getAllChatSessionsFromChatflow` from the chat message API.

---------

Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Vinod Kiran
2025-07-10 20:29:24 +05:30
committed by GitHub
parent 6baec93860
commit bf05f25f7e
55 changed files with 2595 additions and 1560 deletions
@@ -290,6 +290,9 @@ export class DocumentStoreDTO {
} }
static fromEntities(entities: DocumentStore[]): DocumentStoreDTO[] { static fromEntities(entities: DocumentStore[]): DocumentStoreDTO[] {
if (entities.length === 0) {
return []
}
return entities.map((entity) => this.fromEntity(entity)) return entities.map((entity) => this.fromEntity(entity))
} }
@@ -2,12 +2,14 @@ import { Request, Response, NextFunction } from 'express'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import apikeyService from '../../services/apikey' import apikeyService from '../../services/apikey'
import { getPageAndLimitParams } from '../../utils/pagination'
// Get api keys // Get api keys
const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const autoCreateNewKey = true const autoCreateNewKey = true
const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -9,6 +9,7 @@ import { ChatMessage } from '../../database/entities/ChatMessage'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { utilGetChatMessage } from '../../utils/getChatMessage' import { utilGetChatMessage } from '../../utils/getChatMessage'
import { getPageAndLimitParams } from '../../utils/pagination'
const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => { const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => {
try { try {
@@ -71,6 +72,9 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio
const startDate = req.query?.startDate as string | undefined const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined const endDate = req.query?.endDate as string | undefined
const feedback = req.query?.feedback as boolean | undefined const feedback = req.query?.feedback as boolean | undefined
const { page, limit } = getPageAndLimitParams(req)
let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (feedbackTypeFilters) { if (feedbackTypeFilters) {
feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters)
@@ -93,7 +97,9 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio
messageId, messageId,
feedback, feedback,
feedbackTypeFilters, feedbackTypeFilters,
activeWorkspaceId activeWorkspaceId,
page,
limit
) )
return res.json(parseAPIResponse(apiResponse)) return res.json(parseAPIResponse(apiResponse))
} catch (error) { } catch (error) {
@@ -202,7 +208,8 @@ const removeAllChatMessages = async (req: Request, res: Response, next: NextFunc
startDate, startDate,
endDate, endDate,
feedback: isFeedback, feedback: isFeedback,
feedbackTypes: feedbackTypeFilters feedbackTypes: feedbackTypeFilters,
activeWorkspaceId: workspaceId
}) })
const messageIds = messages.map((message) => message.id) const messageIds = messages.map((message) => message.id)
@@ -8,6 +8,7 @@ import chatflowsService from '../../services/chatflows'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { checkUsageLimit } from '../../utils/quotaUsage' import { checkUsageLimit } from '../../utils/quotaUsage'
import { RateLimiterManager } from '../../utils/rateLimit' import { RateLimiterManager } from '../../utils/rateLimit'
import { getPageAndLimitParams } from '../../utils/pagination'
const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => { const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -67,7 +68,14 @@ const deleteChatflow = async (req: Request, res: Response, next: NextFunction) =
const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => { const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await chatflowsService.getAllChatflows(req.query?.type as ChatflowType, req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await chatflowsService.getAllChatflows(
req.query?.type as ChatflowType,
req.user?.activeWorkspaceId,
page,
limit
)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -2,10 +2,12 @@ import { Request, Response, NextFunction } from 'express'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import datasetService from '../../services/dataset' import datasetService from '../../services/dataset'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { getPageAndLimitParams } from '../../utils/pagination'
const getAllDatasets = async (req: Request, res: Response, next: NextFunction) => { const getAllDatasets = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await datasetService.getAllDatasets(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await datasetService.getAllDatasets(req.user?.activeWorkspaceId, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -17,7 +19,8 @@ const getDataset = async (req: Request, res: Response, next: NextFunction) => {
if (typeof req.params === 'undefined' || !req.params.id) { if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: datasetService.getDataset - id not provided!`) throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: datasetService.getDataset - id not provided!`)
} }
const apiResponse = await datasetService.getDataset(req.params.id) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await datasetService.getDataset(req.params.id, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -6,6 +6,7 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { DocumentStoreDTO } from '../../Interface' import { DocumentStoreDTO } from '../../Interface'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { FLOWISE_COUNTER_STATUS, FLOWISE_METRIC_COUNTERS } from '../../Interface.Metrics' import { FLOWISE_COUNTER_STATUS, FLOWISE_METRIC_COUNTERS } from '../../Interface.Metrics'
import { getPageAndLimitParams } from '../../utils/pagination'
const createDocumentStore = async (req: Request, res: Response, next: NextFunction) => { const createDocumentStore = async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -37,8 +38,17 @@ const createDocumentStore = async (req: Request, res: Response, next: NextFuncti
const getAllDocumentStores = async (req: Request, res: Response, next: NextFunction) => { const getAllDocumentStores = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await documentStoreService.getAllDocumentStores(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse: any = await documentStoreService.getAllDocumentStores(req.user?.activeWorkspaceId, page, limit)
if (apiResponse?.total >= 0) {
return res.json({
total: apiResponse.total,
data: DocumentStoreDTO.fromEntities(apiResponse.data)
})
} else {
return res.json(DocumentStoreDTO.fromEntities(apiResponse)) return res.json(DocumentStoreDTO.fromEntities(apiResponse))
}
} catch (error) { } catch (error) {
next(error) next(error)
} }
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from 'express'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import evaluationsService from '../../services/evaluations' import evaluationsService from '../../services/evaluations'
import { getPageAndLimitParams } from '../../utils/pagination'
const createEvaluation = async (req: Request, res: Response, next: NextFunction) => { const createEvaluation = async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -81,7 +82,8 @@ const deleteEvaluation = async (req: Request, res: Response, next: NextFunction)
const getAllEvaluations = async (req: Request, res: Response, next: NextFunction) => { const getAllEvaluations = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await evaluationsService.getAllEvaluations(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await evaluationsService.getAllEvaluations(req.user?.activeWorkspaceId, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -2,10 +2,12 @@ import { Request, Response, NextFunction } from 'express'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import evaluatorService from '../../services/evaluator' import evaluatorService from '../../services/evaluator'
import { getPageAndLimitParams } from '../../utils/pagination'
const getAllEvaluators = async (req: Request, res: Response, next: NextFunction) => { const getAllEvaluators = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await evaluatorService.getAllEvaluators(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await evaluatorService.getAllEvaluators(req.user?.activeWorkspaceId, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
+10 -1
View File
@@ -45,7 +45,16 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction)
return res.status(500).send(e) return res.status(500).send(e)
} }
} }
const apiResponse = await statsService.getChatflowStats(chatflowid, chatTypes, startDate, endDate, '', true, feedbackTypeFilters) const apiResponse = await statsService.getChatflowStats(
chatflowid,
chatTypes,
startDate,
endDate,
'',
true,
feedbackTypeFilters,
req.user?.activeWorkspaceId
)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from 'express'
import toolsService from '../../services/tools' import toolsService from '../../services/tools'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { getPageAndLimitParams } from '../../utils/pagination'
const createTool = async (req: Request, res: Response, next: NextFunction) => { const createTool = async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -40,7 +41,8 @@ const deleteTool = async (req: Request, res: Response, next: NextFunction) => {
const getAllTools = async (req: Request, res: Response, next: NextFunction) => { const getAllTools = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await toolsService.getAllTools(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await toolsService.getAllTools(req.user?.activeWorkspaceId, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -3,6 +3,7 @@ import variablesService from '../../services/variables'
import { Variable } from '../../database/entities/Variable' import { Variable } from '../../database/entities/Variable'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { getPageAndLimitParams } from '../../utils/pagination'
const createVariable = async (req: Request, res: Response, next: NextFunction) => { const createVariable = async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -45,7 +46,8 @@ const deleteVariable = async (req: Request, res: Response, next: NextFunction) =
const getAllVariables = async (req: Request, res: Response, next: NextFunction) => { const getAllVariables = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const apiResponse = await variablesService.getAllVariables(req.user?.activeWorkspaceId) const { page, limit } = getPageAndLimitParams(req)
const apiResponse = await variablesService.getAllVariables(req.user?.activeWorkspaceId, page, limit)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
+6 -9
View File
@@ -21,7 +21,7 @@ import { WHITELIST_URLS } from './utils/constants'
import { initializeJwtCookieMiddleware, verifyToken } from './enterprise/middleware/passport' import { initializeJwtCookieMiddleware, verifyToken } from './enterprise/middleware/passport'
import { IdentityManager } from './IdentityManager' import { IdentityManager } from './IdentityManager'
import { SSEStreamer } from './utils/SSEStreamer' import { SSEStreamer } from './utils/SSEStreamer'
import { getAPIKeyWorkspaceID, validateAPIKey } from './utils/validateKey' import { validateAPIKey } from './utils/validateKey'
import { LoggedInUser } from './enterprise/Interface.Enterprise' import { LoggedInUser } from './enterprise/Interface.Enterprise'
import { IMetricsProvider } from './Interface.Metrics' import { IMetricsProvider } from './Interface.Metrics'
import { Prometheus } from './metrics/Prometheus' import { Prometheus } from './metrics/Prometheus'
@@ -217,12 +217,12 @@ export class App {
return res.status(401).json({ error: 'Unauthorized Access' }) return res.status(401).json({ error: 'Unauthorized Access' })
} }
} }
const isKeyValidated = await validateAPIKey(req)
if (!isKeyValidated) { const { isValid, workspaceId: apiKeyWorkSpaceId } = await validateAPIKey(req)
if (!isValid) {
return res.status(401).json({ error: 'Unauthorized Access' }) return res.status(401).json({ error: 'Unauthorized Access' })
} }
const apiKeyWorkSpaceId = await getAPIKeyWorkspaceID(req)
if (apiKeyWorkSpaceId) {
// Find workspace // Find workspace
const workspace = await this.AppDataSource.getRepository(Workspace).findOne({ const workspace = await this.AppDataSource.getRepository(Workspace).findOne({
where: { id: apiKeyWorkSpaceId } where: { id: apiKeyWorkSpaceId }
@@ -261,14 +261,11 @@ export class App {
activeOrganizationCustomerId: customerId, activeOrganizationCustomerId: customerId,
activeOrganizationProductId: productId, activeOrganizationProductId: productId,
isOrganizationAdmin: true, isOrganizationAdmin: true,
activeWorkspaceId: apiKeyWorkSpaceId, activeWorkspaceId: apiKeyWorkSpaceId!,
activeWorkspace: workspace.name, activeWorkspace: workspace.name,
isApiKeyValidated: true isApiKeyValidated: true
} }
next() next()
} else {
return res.status(401).json({ error: 'Unauthorized Access' })
}
} }
} else { } else {
return res.status(401).json({ error: 'Unauthorized Access' }) return res.status(401).json({ error: 'Unauthorized Access' })
+35 -7
View File
@@ -9,19 +9,31 @@ import { Not, IsNull } from 'typeorm'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
const getAllApiKeysFromDB = async (workspaceId?: string) => { const getAllApiKeysFromDB = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const keys = await appServer.AppDataSource.getRepository(ApiKey).findBy(getWorkspaceSearchOptions(workspaceId)) const queryBuilder = appServer.AppDataSource.getRepository(ApiKey).createQueryBuilder('api_key').orderBy('api_key.updatedDate', 'DESC')
const keysWithChatflows = await addChatflowsCount(keys) if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (workspaceId) queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
const keysWithChatflows = await addChatflowsCount(data)
if (page > 0 && limit > 0) {
return { total, data: keysWithChatflows }
} else {
return keysWithChatflows return keysWithChatflows
}
} }
const getAllApiKeys = async (workspaceId?: string, autoCreateNewKey?: boolean) => { const getAllApiKeys = async (workspaceId?: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => {
try { try {
let keys = await getAllApiKeysFromDB(workspaceId) let keys = await getAllApiKeysFromDB(workspaceId, page, limit)
if (keys.length === 0 && autoCreateNewKey) { const isEmpty = keys?.total === 0 || (Array.isArray(keys) && keys?.length === 0)
if (isEmpty && autoCreateNewKey) {
await createApiKey('DefaultKey', workspaceId) await createApiKey('DefaultKey', workspaceId)
keys = await getAllApiKeysFromDB(workspaceId) keys = await getAllApiKeysFromDB(workspaceId, page, limit)
} }
return keys return keys
} catch (error) { } catch (error) {
@@ -44,6 +56,21 @@ const getApiKey = async (apiKey: string) => {
} }
} }
const getApiKeyById = async (apiKeyId: string) => {
try {
const appServer = getRunningExpressApp()
const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({
id: apiKeyId
})
if (!currentKey) {
return undefined
}
return currentKey
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getApiKeyById - ${getErrorMessage(error)}`)
}
}
const createApiKey = async (keyName: string, workspaceId?: string) => { const createApiKey = async (keyName: string, workspaceId?: string) => {
try { try {
const apiKey = generateAPIKey() const apiKey = generateAPIKey()
@@ -231,5 +258,6 @@ export default {
updateApiKey, updateApiKey,
verifyApiKey, verifyApiKey,
getApiKey, getApiKey,
getApiKeyById,
importKeys importKeys
} }
@@ -39,7 +39,9 @@ const getAllChatMessages = async (
messageId?: string, messageId?: string,
feedback?: boolean, feedback?: boolean,
feedbackTypes?: ChatMessageRatingType[], feedbackTypes?: ChatMessageRatingType[],
activeWorkspaceId?: string activeWorkspaceId?: string,
page?: number,
pageSize?: number
): Promise<ChatMessage[]> => { ): Promise<ChatMessage[]> => {
try { try {
const dbResponse = await utilGetChatMessage({ const dbResponse = await utilGetChatMessage({
@@ -54,7 +56,9 @@ const getAllChatMessages = async (
messageId, messageId,
feedback, feedback,
feedbackTypes, feedbackTypes,
activeWorkspaceId activeWorkspaceId,
page,
pageSize
}) })
return dbResponse return dbResponse
} catch (error) { } catch (error) {
@@ -127,21 +127,36 @@ const deleteChatflow = async (chatflowId: string, orgId: string, workspaceId: st
} }
} }
const getAllChatflows = async (type?: ChatflowType, workspaceId?: string): Promise<ChatFlow[]> => { const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findBy(getWorkspaceSearchOptions(workspaceId))
const queryBuilder = appServer.AppDataSource.getRepository(ChatFlow)
.createQueryBuilder('chat_flow')
.orderBy('chat_flow.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (type === 'MULTIAGENT') { if (type === 'MULTIAGENT') {
return dbResponse.filter((chatflow) => chatflow.type === 'MULTIAGENT') queryBuilder.andWhere('chat_flow.type = :type', { type: 'MULTIAGENT' })
} else if (type === 'AGENTFLOW') { } else if (type === 'AGENTFLOW') {
return dbResponse.filter((chatflow) => chatflow.type === 'AGENTFLOW') queryBuilder.andWhere('chat_flow.type = :type', { type: 'AGENTFLOW' })
} else if (type === 'ASSISTANT') { } else if (type === 'ASSISTANT') {
return dbResponse.filter((chatflow) => chatflow.type === 'ASSISTANT') queryBuilder.andWhere('chat_flow.type = :type', { type: 'ASSISTANT' })
} else if (type === 'CHATFLOW') { } else if (type === 'CHATFLOW') {
// fetch all chatflows that are not agentflow // fetch all chatflows that are not agentflow
return dbResponse.filter((chatflow) => chatflow.type === 'CHATFLOW' || !chatflow.type) queryBuilder.andWhere('chat_flow.type = :type', { type: 'CHATFLOW' })
}
if (workspaceId) queryBuilder.andWhere('chat_flow.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
if (page > 0 && limit > 0) {
return { data, total }
} else {
return data
} }
return dbResponse
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
+36 -16
View File
@@ -8,22 +8,33 @@ import { Readable } from 'stream'
import { In } from 'typeorm' import { In } from 'typeorm'
import csv from 'csv-parser' import csv from 'csv-parser'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
const getAllDatasets = async (workspaceId?: string) => { const getAllDatasets = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const queryBuilder = appServer.AppDataSource.getRepository(Dataset).createQueryBuilder('ds').orderBy('ds.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (workspaceId) queryBuilder.andWhere('ds.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
const returnObj: Dataset[] = [] const returnObj: Dataset[] = []
const datasets = await appServer.AppDataSource.getRepository(Dataset).findBy(getWorkspaceSearchOptions(workspaceId))
// TODO: This is a hack to get the row count for each dataset. Need to find a better way to do this // TODO: This is a hack to get the row count for each dataset. Need to find a better way to do this
for (const dataset of datasets) { for (const dataset of data) {
;(dataset as any).rowCount = await appServer.AppDataSource.getRepository(DatasetRow).count({ ;(dataset as any).rowCount = await appServer.AppDataSource.getRepository(DatasetRow).count({
where: { datasetId: dataset.id } where: { datasetId: dataset.id }
}) })
returnObj.push(dataset) returnObj.push(dataset)
} }
if (page > 0 && limit > 0) {
return { total, data: returnObj }
} else {
return returnObj return returnObj
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
@@ -32,36 +43,45 @@ const getAllDatasets = async (workspaceId?: string) => {
} }
} }
const getDataset = async (id: string) => { const getDataset = async (id: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const dataset = await appServer.AppDataSource.getRepository(Dataset).findOneBy({ const dataset = await appServer.AppDataSource.getRepository(Dataset).findOneBy({
id: id id: id
}) })
let items = await appServer.AppDataSource.getRepository(DatasetRow).find({ const queryBuilder = appServer.AppDataSource.getRepository(DatasetRow).createQueryBuilder('dsr').orderBy('dsr.sequenceNo', 'ASC')
where: { datasetId: id }, queryBuilder.andWhere('dsr.datasetId = :datasetId', { datasetId: id })
order: { sequenceNo: 'asc' } if (page > 0 && limit > 0) {
}) queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
let [data, total] = await queryBuilder.getManyAndCount()
// special case for sequence numbers == -1 (this happens when the update script is run and all rows are set to -1) // special case for sequence numbers == -1 (this happens when the update script is run and all rows are set to -1)
// check if there are any sequence numbers == -1, if so set them to the max sequence number + 1 // check if there are any sequence numbers == -1, if so set them to the max sequence number + 1
const missingSequenceNumbers = items.filter((item) => item.sequenceNo === -1) const missingSequenceNumbers = data.filter((item) => item.sequenceNo === -1)
if (missingSequenceNumbers.length > 0) { if (missingSequenceNumbers.length > 0) {
const maxSequenceNumber = items.reduce((prev, current) => (prev.sequenceNo > current.sequenceNo ? prev : current)) const maxSequenceNumber = data.reduce((prev, current) => (prev.sequenceNo > current.sequenceNo ? prev : current))
let sequenceNo = maxSequenceNumber.sequenceNo + 1 let sequenceNo = maxSequenceNumber.sequenceNo + 1
for (const zeroSequenceNumber of missingSequenceNumbers) { for (const zeroSequenceNumber of missingSequenceNumbers) {
zeroSequenceNumber.sequenceNo = sequenceNo++ zeroSequenceNumber.sequenceNo = sequenceNo++
} }
await appServer.AppDataSource.getRepository(DatasetRow).save(missingSequenceNumbers) await appServer.AppDataSource.getRepository(DatasetRow).save(missingSequenceNumbers)
// now get the items again // now get the items again
items = await appServer.AppDataSource.getRepository(DatasetRow).find({ const queryBuilder2 = appServer.AppDataSource.getRepository(DatasetRow)
where: { datasetId: id }, .createQueryBuilder('dsr')
order: { sequenceNo: 'asc' } .orderBy('dsr.sequenceNo', 'ASC')
}) queryBuilder2.andWhere('dsr.datasetId = :datasetId', { datasetId: id })
if (page > 0 && limit > 0) {
queryBuilder2.skip((page - 1) * limit)
queryBuilder2.take(limit)
}
;[data, total] = await queryBuilder2.getManyAndCount()
} }
return { return {
...dataset, ...dataset,
rows: items rows: data,
total
} }
} catch (error) { } catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: datasetService.getDataset - ${getErrorMessage(error)}`) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: datasetService.getDataset - ${getErrorMessage(error)}`)
@@ -77,11 +77,26 @@ const createDocumentStore = async (newDocumentStore: DocumentStore, orgId: strin
} }
} }
const getAllDocumentStores = async (workspaceId?: string) => { const getAllDocumentStores = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const entities = await appServer.AppDataSource.getRepository(DocumentStore).findBy(getWorkspaceSearchOptions(workspaceId)) const queryBuilder = appServer.AppDataSource.getRepository(DocumentStore)
return entities .createQueryBuilder('doc_store')
.orderBy('doc_store.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (workspaceId) queryBuilder.andWhere('doc_store.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
if (page > 0 && limit > 0) {
return { data, total }
} else {
return data
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
@@ -338,42 +338,80 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str
} }
} }
const getAllEvaluations = async (workspaceId?: string) => { const getAllEvaluations = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const findAndOrderBy: any = {
where: getWorkspaceSearchOptions(workspaceId),
order: {
runDate: 'DESC'
}
}
const evaluations = await appServer.AppDataSource.getRepository(Evaluation).find(findAndOrderBy)
// First, get the count of distinct evaluation names for the total
// needed as the The getCount() method in TypeORM doesn't respect the GROUP BY clause and will return the total count of records
const countQuery = appServer.AppDataSource.getRepository(Evaluation)
.createQueryBuilder('ev')
.select('COUNT(DISTINCT(ev.name))', 'count')
.where('ev.workspaceId = :workspaceId', { workspaceId: workspaceId })
const totalResult = await countQuery.getRawOne()
const total = totalResult ? parseInt(totalResult.count) : 0
// Then get the distinct evaluation names with their counts and latest run date
const namesQueryBuilder = appServer.AppDataSource.getRepository(Evaluation)
.createQueryBuilder('ev')
.select('DISTINCT(ev.name)', 'name')
.addSelect('COUNT(ev.name)', 'count')
.addSelect('MAX(ev.runDate)', 'latestRunDate')
.andWhere('ev.workspaceId = :workspaceId', { workspaceId: workspaceId })
.groupBy('ev.name')
.orderBy('max(ev.runDate)', 'DESC') // Order by the latest run date
if (page > 0 && limit > 0) {
namesQueryBuilder.skip((page - 1) * limit)
namesQueryBuilder.take(limit)
}
const evaluationNames = await namesQueryBuilder.getRawMany()
// Get all evaluations for all names at once in a single query
const returnResults: IEvaluationResult[] = [] const returnResults: IEvaluationResult[] = []
// mark the first evaluation with a unique name as the latestEval and then reset the version number
for (let i = 0; i < evaluations.length; i++) { if (evaluationNames.length > 0) {
const evaluation = evaluations[i] as IEvaluationResult const names = evaluationNames.map((item) => item.name)
// Fetch all evaluations for these names in a single query
const allEvaluations = await appServer.AppDataSource.getRepository(Evaluation)
.createQueryBuilder('ev')
.where('ev.name IN (:...names)', { names })
.andWhere('ev.workspaceId = :workspaceId', { workspaceId })
.orderBy('ev.name', 'ASC')
.addOrderBy('ev.runDate', 'DESC')
.getMany()
// Process the results by name
const evaluationsByName = new Map<string, Evaluation[]>()
// Group evaluations by name
for (const evaluation of allEvaluations) {
if (!evaluationsByName.has(evaluation.name)) {
evaluationsByName.set(evaluation.name, [])
}
evaluationsByName.get(evaluation.name)!.push(evaluation)
}
// Process each name's evaluations
for (const item of evaluationNames) {
const evaluationsForName = evaluationsByName.get(item.name) || []
for (let i = 0; i < evaluationsForName.length; i++) {
const evaluation = evaluationsForName[i] as IEvaluationResult
evaluation.latestEval = i === 0
evaluation.version = parseInt(item.count) - i
returnResults.push(evaluation) returnResults.push(evaluation)
// find the first index with this name in the evaluations array
// as it is sorted desc, make the first evaluation with this name as the latestEval
const currentIndex = evaluations.indexOf(evaluation)
if (evaluations.findIndex((e) => e.name === evaluation.name) === currentIndex) {
returnResults[i].latestEval = true
}
}
for (let i = 0; i < returnResults.length; i++) {
const evaluation = returnResults[i]
if (evaluation.latestEval) {
const versions = returnResults.filter((e) => e.name === evaluation.name)
let descVersion = versions.length
for (let j = 0; j < versions.length; j++) {
versions[j].version = descVersion--
} }
} }
} }
if (page > 0 && limit > 0) {
return {
total: total,
data: returnResults
}
} else {
return returnResults return returnResults
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
@@ -4,13 +4,25 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils' import { getErrorMessage } from '../../errors/utils'
import { Evaluator } from '../../database/entities/Evaluator' import { Evaluator } from '../../database/entities/Evaluator'
import { EvaluatorDTO } from '../../Interface.Evaluation' import { EvaluatorDTO } from '../../Interface.Evaluation'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
const getAllEvaluators = async (workspaceId?: string) => { const getAllEvaluators = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const results: Evaluator[] = await appServer.AppDataSource.getRepository(Evaluator).findBy(getWorkspaceSearchOptions(workspaceId)) const queryBuilder = appServer.AppDataSource.getRepository(Evaluator).createQueryBuilder('ev').orderBy('ev.updatedDate', 'DESC')
return EvaluatorDTO.fromEntities(results) if (workspaceId) queryBuilder.andWhere('ev.workspaceId = :workspaceId', { workspaceId })
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
const [data, total] = await queryBuilder.getManyAndCount()
if (page > 0 && limit > 0) {
return {
total,
data: EvaluatorDTO.fromEntities(data)
}
} else {
return EvaluatorDTO.fromEntities(data)
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
@@ -65,7 +65,7 @@ const getPublicExecutionById = async (executionId: string): Promise<Execution |
const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data: Execution[]; total: number }> => { const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data: Execution[]; total: number }> => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const { id, agentflowId, sessionId, state, startDate, endDate, page = 1, limit = 10, workspaceId } = filters const { id, agentflowId, sessionId, state, startDate, endDate, page = 1, limit = 12, workspaceId } = filters
// Handle UUID fields properly using raw parameters to avoid type conversion issues // Handle UUID fields properly using raw parameters to avoid type conversion issues
// This uses the query builder instead of direct objects for compatibility with UUID fields // This uses the query builder instead of direct objects for compatibility with UUID fields
@@ -90,16 +90,20 @@ const convertExportInput = (body: any): ExportInput => {
const FileDefaultName = 'ExportData.json' const FileDefaultName = 'ExportData.json'
const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): Promise<{ FileDefaultName: string } & ExportData> => { const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): Promise<{ FileDefaultName: string } & ExportData> => {
try { try {
let AgentFlow: ChatFlow[] = let AgentFlow: ChatFlow[] | { data: ChatFlow[]; total: number } =
exportInput.agentflow === true ? await chatflowService.getAllChatflows('MULTIAGENT', activeWorkspaceId) : [] exportInput.agentflow === true ? await chatflowService.getAllChatflows('MULTIAGENT', activeWorkspaceId) : []
AgentFlow = 'data' in AgentFlow ? AgentFlow.data : AgentFlow
let AgentFlowV2: ChatFlow[] = let AgentFlowV2: ChatFlow[] | { data: ChatFlow[]; total: number } =
exportInput.agentflowv2 === true ? await chatflowService.getAllChatflows('AGENTFLOW', activeWorkspaceId) : [] exportInput.agentflowv2 === true ? await chatflowService.getAllChatflows('AGENTFLOW', activeWorkspaceId) : []
AgentFlowV2 = 'data' in AgentFlowV2 ? AgentFlowV2.data : AgentFlowV2
let AssistantCustom: Assistant[] = let AssistantCustom: Assistant[] =
exportInput.assistantCustom === true ? await assistantService.getAllAssistants('CUSTOM', activeWorkspaceId) : [] exportInput.assistantCustom === true ? await assistantService.getAllAssistants('CUSTOM', activeWorkspaceId) : []
let AssistantFlow: ChatFlow[] =
let AssistantFlow: ChatFlow[] | { data: ChatFlow[]; total: number } =
exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('ASSISTANT', activeWorkspaceId) : [] exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('ASSISTANT', activeWorkspaceId) : []
AssistantFlow = 'data' in AssistantFlow ? AssistantFlow.data : AssistantFlow
let AssistantOpenAI: Assistant[] = let AssistantOpenAI: Assistant[] =
exportInput.assistantOpenAI === true ? await assistantService.getAllAssistants('OPENAI', activeWorkspaceId) : [] exportInput.assistantOpenAI === true ? await assistantService.getAllAssistants('OPENAI', activeWorkspaceId) : []
@@ -107,12 +111,15 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string):
let AssistantAzure: Assistant[] = let AssistantAzure: Assistant[] =
exportInput.assistantAzure === true ? await assistantService.getAllAssistants('AZURE', activeWorkspaceId) : [] exportInput.assistantAzure === true ? await assistantService.getAllAssistants('AZURE', activeWorkspaceId) : []
let ChatFlow: ChatFlow[] = exportInput.chatflow === true ? await chatflowService.getAllChatflows('CHATFLOW', activeWorkspaceId) : [] let ChatFlow: ChatFlow[] | { data: ChatFlow[]; total: number } =
exportInput.chatflow === true ? await chatflowService.getAllChatflows('CHATFLOW', activeWorkspaceId) : []
ChatFlow = 'data' in ChatFlow ? ChatFlow.data : ChatFlow
const allChatflow: ChatFlow[] = let allChatflow: ChatFlow[] | { data: ChatFlow[]; total: number } =
exportInput.chat_message === true || exportInput.chat_feedback === true exportInput.chat_message === true || exportInput.chat_feedback === true
? await chatflowService.getAllChatflows(undefined, activeWorkspaceId) ? await chatflowService.getAllChatflows(undefined, activeWorkspaceId)
: [] : []
allChatflow = 'data' in allChatflow ? allChatflow.data : allChatflow
const chatflowIds = allChatflow.map((chatflow) => chatflow.id) const chatflowIds = allChatflow.map((chatflow) => chatflow.id)
let ChatMessage: ChatMessage[] = let ChatMessage: ChatMessage[] =
@@ -124,8 +131,10 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string):
let CustomTemplate: CustomTemplate[] = let CustomTemplate: CustomTemplate[] =
exportInput.custom_template === true ? await marketplacesService.getAllCustomTemplates(activeWorkspaceId) : [] exportInput.custom_template === true ? await marketplacesService.getAllCustomTemplates(activeWorkspaceId) : []
let DocumentStore: DocumentStore[] = let DocumentStore: DocumentStore[] | { data: DocumentStore[]; total: number } =
exportInput.document_store === true ? await documenStoreService.getAllDocumentStores(activeWorkspaceId) : [] exportInput.document_store === true ? await documenStoreService.getAllDocumentStores(activeWorkspaceId) : []
DocumentStore = 'data' in DocumentStore ? DocumentStore.data : DocumentStore
const documentStoreIds = DocumentStore.map((documentStore) => documentStore.id) const documentStoreIds = DocumentStore.map((documentStore) => documentStore.id)
let DocumentStoreFileChunk: DocumentStoreFileChunk[] = let DocumentStoreFileChunk: DocumentStoreFileChunk[] =
@@ -137,9 +146,13 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string):
const { data: totalExecutions } = exportInput.execution === true ? await executionService.getAllExecutions(filters) : { data: [] } const { data: totalExecutions } = exportInput.execution === true ? await executionService.getAllExecutions(filters) : { data: [] }
let Execution: Execution[] = exportInput.execution === true ? totalExecutions : [] let Execution: Execution[] = exportInput.execution === true ? totalExecutions : []
let Tool: Tool[] = exportInput.tool === true ? await toolsService.getAllTools(activeWorkspaceId) : [] let Tool: Tool[] | { data: Tool[]; total: number } =
exportInput.tool === true ? await toolsService.getAllTools(activeWorkspaceId) : []
Tool = 'data' in Tool ? Tool.data : Tool
let Variable: Variable[] = exportInput.variable === true ? await variableService.getAllVariables(activeWorkspaceId) : [] let Variable: Variable[] | { data: Variable[]; total: number } =
exportInput.variable === true ? await variableService.getAllVariables(activeWorkspaceId) : []
Variable = 'data' in Variable ? Variable.data : Variable
return { return {
FileDefaultName, FileDefaultName,
+9 -3
View File
@@ -14,7 +14,8 @@ const getChatflowStats = async (
endDate?: string, endDate?: string,
messageId?: string, messageId?: string,
feedback?: boolean, feedback?: boolean,
feedbackTypes?: ChatMessageRatingType[] feedbackTypes?: ChatMessageRatingType[],
activeWorkspaceId?: string
): Promise<any> => { ): Promise<any> => {
try { try {
const chatmessages = (await utilGetChatMessage({ const chatmessages = (await utilGetChatMessage({
@@ -24,15 +25,20 @@ const getChatflowStats = async (
endDate, endDate,
messageId, messageId,
feedback, feedback,
feedbackTypes feedbackTypes,
activeWorkspaceId
})) as Array<ChatMessage & { feedback?: ChatMessageFeedback }> })) as Array<ChatMessage & { feedback?: ChatMessageFeedback }>
const totalMessages = chatmessages.length const totalMessages = chatmessages.length
const totalFeedback = chatmessages.filter((message) => message?.feedback).length const totalFeedback = chatmessages.filter((message) => message?.feedback).length
const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length
// count the number of unique sessions in the chatmessages - count unique sessionId
const uniqueSessions = new Set(chatmessages.map((message) => message.sessionId))
const totalSessions = uniqueSessions.size
const dbResponse = { const dbResponse = {
totalMessages, totalMessages,
totalFeedback, totalFeedback,
positiveFeedback positiveFeedback,
totalSessions
} }
return dbResponse return dbResponse
+15 -4
View File
@@ -3,7 +3,6 @@ import { Tool } from '../../database/entities/Tool'
import { getAppVersion } from '../../utils' import { getAppVersion } from '../../utils'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils' import { getErrorMessage } from '../../errors/utils'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { FLOWISE_METRIC_COUNTERS, FLOWISE_COUNTER_STATUS } from '../../Interface.Metrics' import { FLOWISE_METRIC_COUNTERS, FLOWISE_COUNTER_STATUS } from '../../Interface.Metrics'
import { QueryRunner } from 'typeorm' import { QueryRunner } from 'typeorm'
@@ -44,11 +43,23 @@ const deleteTool = async (toolId: string): Promise<any> => {
} }
} }
const getAllTools = async (workspaceId?: string): Promise<Tool[]> => { const getAllTools = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(Tool).findBy(getWorkspaceSearchOptions(workspaceId)) const queryBuilder = appServer.AppDataSource.getRepository(Tool).createQueryBuilder('tool').orderBy('tool.updatedDate', 'DESC')
return dbResponse
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (workspaceId) queryBuilder.andWhere('tool.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
if (page > 0 && limit > 0) {
return { data, total }
} else {
return data
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: toolsService.getAllTools - ${getErrorMessage(error)}`) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: toolsService.getAllTools - ${getErrorMessage(error)}`)
} }
@@ -4,7 +4,6 @@ import { Variable } from '../../database/entities/Variable'
import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils' import { getErrorMessage } from '../../errors/utils'
import { getAppVersion } from '../../utils' import { getAppVersion } from '../../utils'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
import { QueryRunner } from 'typeorm' import { QueryRunner } from 'typeorm'
import { validate } from 'uuid' import { validate } from 'uuid'
@@ -44,11 +43,26 @@ const deleteVariable = async (variableId: string): Promise<any> => {
} }
} }
const getAllVariables = async (workspaceId?: string) => { const getAllVariables = async (workspaceId?: string, page: number = -1, limit: number = -1) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(Variable).findBy(getWorkspaceSearchOptions(workspaceId)) const queryBuilder = appServer.AppDataSource.getRepository(Variable)
return dbResponse .createQueryBuilder('variable')
.orderBy('variable.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (workspaceId) queryBuilder.andWhere('variable.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
if (page > 0 && limit > 0) {
return { data, total }
} else {
return data
}
} catch (error) { } catch (error) {
throw new InternalFlowiseError( throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR,
+2 -2
View File
@@ -57,7 +57,7 @@ import {
constructGraphs, constructGraphs,
getAPIOverrideConfig getAPIOverrideConfig
} from '../utils' } from '../utils'
import { validateChatflowAPIKey } from './validateKey' import { validateFlowAPIKey } from './validateKey'
import logger from './logger' import logger from './logger'
import { utilAddChatMessage } from './addChatMesage' import { utilAddChatMessage } from './addChatMesage'
import { checkPredictions, checkStorage, updatePredictionsUsage, updateStorageUsage } from './quotaUsage' import { checkPredictions, checkStorage, updatePredictionsUsage, updateStorageUsage } from './quotaUsage'
@@ -923,7 +923,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
try { try {
// Validate API Key if its external API request // Validate API Key if its external API request
if (!isInternal) { if (!isInternal) {
const isKeyValidated = await validateChatflowAPIKey(req, chatflow) const isKeyValidated = await validateFlowAPIKey(req, chatflow)
if (!isKeyValidated) { if (!isKeyValidated) {
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`)
} }
+251 -54
View File
@@ -4,7 +4,6 @@ import { ChatMessage } from '../database/entities/ChatMessage'
import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
import { ChatFlow } from '../database/entities/ChatFlow' import { ChatFlow } from '../database/entities/ChatFlow'
import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { aMonthAgo } from '.'
/** /**
* Method that get chat messages. * Method that get chat messages.
@@ -19,6 +18,7 @@ import { aMonthAgo } from '.'
* @param {boolean} feedback * @param {boolean} feedback
* @param {ChatMessageRatingType[]} feedbackTypes * @param {ChatMessageRatingType[]} feedbackTypes
*/ */
interface GetChatMessageParams { interface GetChatMessageParams {
chatflowid: string chatflowid: string
chatTypes?: ChatType[] chatTypes?: ChatType[]
@@ -32,6 +32,8 @@ interface GetChatMessageParams {
feedback?: boolean feedback?: boolean
feedbackTypes?: ChatMessageRatingType[] feedbackTypes?: ChatMessageRatingType[]
activeWorkspaceId?: string activeWorkspaceId?: string
page?: number
pageSize?: number
} }
export const utilGetChatMessage = async ({ export const utilGetChatMessage = async ({
@@ -46,72 +48,44 @@ export const utilGetChatMessage = async ({
messageId, messageId,
feedback, feedback,
feedbackTypes, feedbackTypes,
activeWorkspaceId activeWorkspaceId,
page = -1,
pageSize = -1
}: GetChatMessageParams): Promise<ChatMessage[]> => { }: GetChatMessageParams): Promise<ChatMessage[]> => {
if (!page) page = -1
if (!pageSize) pageSize = -1
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
// Check if chatflow workspaceId is same as activeWorkspaceId // Check if chatflow workspaceId is same as activeWorkspaceId
if (activeWorkspaceId) { if (activeWorkspaceId) {
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatflowid id: chatflowid,
workspaceId: activeWorkspaceId
}) })
if (chatflow?.workspaceId !== activeWorkspaceId) { if (!chatflow) {
throw new Error('Unauthorized access') throw new Error('Unauthorized access')
} }
} else {
throw new Error('Unauthorized access')
} }
if (feedback) { if (feedback) {
const query = await appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message') // Handle feedback queries with improved efficiency
return await handleFeedbackQuery({
// do the join with chat message feedback based on messageId for each chat message in the chatflow chatflowid,
query chatTypes,
.leftJoinAndSelect('chat_message.execution', 'execution') sortOrder,
.leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') chatId,
.where('chat_message.chatflowid = :chatflowid', { chatflowid }) memoryType,
sessionId,
// based on which parameters are available add `andWhere` clauses to the query startDate,
if (chatTypes && chatTypes.length > 0) { endDate,
query.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes }) messageId,
} feedbackTypes,
if (chatId) { page,
query.andWhere('chat_message.chatId = :chatId', { chatId }) pageSize
}
if (memoryType) {
query.andWhere('chat_message.memoryType = :memoryType', { memoryType })
}
if (sessionId) {
query.andWhere('chat_message.sessionId = :sessionId', { sessionId })
}
// set date range
if (startDate) {
query.andWhere('chat_message.createdDate >= :startDateTime', { startDateTime: startDate ? new Date(startDate) : aMonthAgo() })
}
if (endDate) {
query.andWhere('chat_message.createdDate <= :endDateTime', { endDateTime: endDate ? new Date(endDate) : new Date() })
}
// sort
query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC')
const messages = (await query.getMany()) as Array<ChatMessage & { feedback: ChatMessageFeedback }>
if (feedbackTypes && feedbackTypes.length > 0) {
// just applying a filter to the messages array will only return the messages that have feedback,
// but we also want the message before the feedback message which is the user message.
const indicesToKeep = new Set()
messages.forEach((message, index) => {
if (message.role === 'apiMessage' && message.feedback && feedbackTypes.includes(message.feedback.rating)) {
if (index > 0) indicesToKeep.add(index - 1)
indicesToKeep.add(index)
}
}) })
return messages.filter((_, index) => indicesToKeep.has(index))
}
return messages
} }
let createdDateQuery let createdDateQuery
@@ -146,3 +120,226 @@ export const utilGetChatMessage = async ({
return messages return messages
} }
async function handleFeedbackQuery(params: {
chatflowid: string
chatTypes?: ChatType[]
sortOrder: string
chatId?: string
memoryType?: string
sessionId?: string
startDate?: string
endDate?: string
messageId?: string
feedbackTypes?: ChatMessageRatingType[]
page: number
pageSize: number
}): Promise<ChatMessage[]> {
const {
chatflowid,
chatTypes,
sortOrder,
chatId,
memoryType,
sessionId,
startDate,
endDate,
messageId,
feedbackTypes,
page,
pageSize
} = params
const appServer = getRunningExpressApp()
// For specific session/message queries, no pagination needed
if (sessionId || messageId) {
return await getMessagesWithFeedback(params, false)
}
// For paginated queries, handle session-based pagination efficiently
if (page > -1 && pageSize > -1) {
// First get session IDs with pagination
const sessionQuery = appServer.AppDataSource.getRepository(ChatMessage)
.createQueryBuilder('chat_message')
.select('DISTINCT chat_message.sessionId', 'sessionId')
.where('chat_message.chatflowid = :chatflowid', { chatflowid })
// Apply basic filters
if (chatTypes && chatTypes.length > 0) {
sessionQuery.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes })
}
if (chatId) {
sessionQuery.andWhere('chat_message.chatId = :chatId', { chatId })
}
if (memoryType) {
sessionQuery.andWhere('chat_message.memoryType = :memoryType', { memoryType })
}
if (startDate && typeof startDate === 'string') {
sessionQuery.andWhere('chat_message.createdDate >= :startDateTime', {
startDateTime: new Date(startDate)
})
}
if (endDate && typeof endDate === 'string') {
sessionQuery.andWhere('chat_message.createdDate <= :endDateTime', {
endDateTime: new Date(endDate)
})
}
// If feedback types are specified, only get sessions with those feedback types
if (feedbackTypes && feedbackTypes.length > 0) {
sessionQuery
.leftJoin(ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
.andWhere('feedback.rating IN (:...feedbackTypes)', { feedbackTypes })
}
const startIndex = pageSize * (page - 1)
const sessionIds = await sessionQuery
.orderBy('MAX(chat_message.createdDate)', sortOrder === 'DESC' ? 'DESC' : 'ASC')
.groupBy('chat_message.sessionId')
.offset(startIndex)
.limit(pageSize)
.getRawMany()
if (sessionIds.length === 0) {
return []
}
// Get all messages for these sessions
const sessionIdList = sessionIds.map((s) => s.sessionId)
return await getMessagesWithFeedback(
{
...params,
sessionId: undefined // Clear specific sessionId since we're using list
},
true,
sessionIdList
)
}
// No pagination - get all feedback messages
return await getMessagesWithFeedback(params, false)
}
async function getMessagesWithFeedback(
params: {
chatflowid: string
chatTypes?: ChatType[]
sortOrder: string
chatId?: string
memoryType?: string
sessionId?: string
startDate?: string
endDate?: string
messageId?: string
feedbackTypes?: ChatMessageRatingType[]
},
useSessionList: boolean = false,
sessionIdList?: string[]
): Promise<ChatMessage[]> {
const { chatflowid, chatTypes, sortOrder, chatId, memoryType, sessionId, startDate, endDate, messageId, feedbackTypes } = params
const appServer = getRunningExpressApp()
const query = appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message')
query
.leftJoinAndSelect('chat_message.execution', 'execution')
.leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
.where('chat_message.chatflowid = :chatflowid', { chatflowid })
// Apply filters
if (useSessionList && sessionIdList && sessionIdList.length > 0) {
query.andWhere('chat_message.sessionId IN (:...sessionIds)', { sessionIds: sessionIdList })
}
if (chatTypes && chatTypes.length > 0) {
query.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes })
}
if (chatId) {
query.andWhere('chat_message.chatId = :chatId', { chatId })
}
if (memoryType) {
query.andWhere('chat_message.memoryType = :memoryType', { memoryType })
}
if (sessionId) {
query.andWhere('chat_message.sessionId = :sessionId', { sessionId })
}
if (messageId) {
query.andWhere('chat_message.id = :messageId', { messageId })
}
if (startDate && typeof startDate === 'string') {
query.andWhere('chat_message.createdDate >= :startDateTime', {
startDateTime: new Date(startDate)
})
}
if (endDate && typeof endDate === 'string') {
query.andWhere('chat_message.createdDate <= :endDateTime', {
endDateTime: new Date(endDate)
})
}
// Pre-filter by feedback types if specified (more efficient than post-processing)
if (feedbackTypes && feedbackTypes.length > 0) {
query.andWhere('(feedback.rating IN (:...feedbackTypes) OR feedback.rating IS NULL)', { feedbackTypes })
}
query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC')
const messages = (await query.getMany()) as Array<ChatMessage & { feedback: ChatMessageFeedback }>
// Apply feedback type filtering with previous message inclusion
if (feedbackTypes && feedbackTypes.length > 0) {
return filterMessagesWithFeedback(messages, feedbackTypes)
}
return messages
}
function filterMessagesWithFeedback(
messages: Array<ChatMessage & { feedback: ChatMessageFeedback }>,
feedbackTypes: ChatMessageRatingType[]
): ChatMessage[] {
// Group messages by session for proper filtering
const sessionGroups = new Map<string, Array<ChatMessage & { feedback: ChatMessageFeedback }>>()
messages.forEach((message) => {
const sessionId = message.sessionId
if (!sessionId) return // Skip messages without sessionId
if (!sessionGroups.has(sessionId)) {
sessionGroups.set(sessionId, [])
}
sessionGroups.get(sessionId)!.push(message)
})
const result: ChatMessage[] = []
// Process each session group
sessionGroups.forEach((sessionMessages) => {
// Sort by creation date to ensure proper order
sessionMessages.sort((a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime())
const toInclude = new Set<number>()
sessionMessages.forEach((message, index) => {
if (message.role === 'apiMessage' && message.feedback && feedbackTypes.includes(message.feedback.rating)) {
// Include the feedback message
toInclude.add(index)
// Include the previous message (user message) if it exists
if (index > 0) {
toInclude.add(index - 1)
}
}
})
// Add filtered messages to result
sessionMessages.forEach((message, index) => {
if (toInclude.has(index)) {
result.push(message)
}
})
})
// Sort final result by creation date
return result.sort((a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime())
}
+29
View File
@@ -0,0 +1,29 @@
import { InternalFlowiseError } from '../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { Request } from 'express'
type Pagination = {
page: number
limit: number
}
export const getPageAndLimitParams = (req: Request): Pagination => {
// by default assume no pagination
let page = -1
let limit = -1
if (req.query.page) {
// if page is provided, make sure it's a positive number
page = parseInt(req.query.page as string)
if (page < 0) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: page cannot be negative!`)
}
}
if (req.query.limit) {
// if limit is provided, make sure it's a positive number
limit = parseInt(req.query.limit as string)
if (limit < 0) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: limit cannot be negative!`)
}
}
return { page, limit }
}
+2 -2
View File
@@ -21,7 +21,7 @@ import {
getStartingNodes, getStartingNodes,
getAPIOverrideConfig getAPIOverrideConfig
} from '../utils' } from '../utils'
import { validateChatflowAPIKey } from './validateKey' import { validateFlowAPIKey } from './validateKey'
import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType, IExecuteFlowParams, MODE } from '../Interface' import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType, IExecuteFlowParams, MODE } from '../Interface'
import { ChatFlow } from '../database/entities/ChatFlow' import { ChatFlow } from '../database/entities/ChatFlow'
import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { getRunningExpressApp } from '../utils/getRunningExpressApp'
@@ -251,7 +251,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) =>
const files = (req.files as Express.Multer.File[]) || [] const files = (req.files as Express.Multer.File[]) || []
if (!isInternal) { if (!isInternal) {
const isKeyValidated = await validateChatflowAPIKey(req, chatflow) const isKeyValidated = await validateFlowAPIKey(req, chatflow)
if (!isKeyValidated) { if (!isKeyValidated) {
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`)
} }
+37 -32
View File
@@ -1,14 +1,15 @@
import { Request } from 'express' import { Request } from 'express'
import { ChatFlow } from '../database/entities/ChatFlow' import { ChatFlow } from '../database/entities/ChatFlow'
import { ApiKey } from '../database/entities/ApiKey'
import { compareKeys } from './apiKey' import { compareKeys } from './apiKey'
import apikeyService from '../services/apikey' import apikeyService from '../services/apikey'
/** /**
* Validate Chatflow API Key * Validate flow API Key, this is needed because Prediction/Upsert API is public
* @param {Request} req * @param {Request} req
* @param {ChatFlow} chatflow * @param {ChatFlow} chatflow
*/ */
export const validateChatflowAPIKey = async (req: Request, chatflow: ChatFlow) => { export const validateFlowAPIKey = async (req: Request, chatflow: ChatFlow): Promise<boolean> => {
const chatFlowApiKeyId = chatflow?.apikeyid const chatFlowApiKeyId = chatflow?.apikeyid
if (!chatFlowApiKeyId) return true if (!chatFlowApiKeyId) return true
@@ -16,48 +17,52 @@ export const validateChatflowAPIKey = async (req: Request, chatflow: ChatFlow) =
if (chatFlowApiKeyId && !authorizationHeader) return false if (chatFlowApiKeyId && !authorizationHeader) return false
const suppliedKey = authorizationHeader.split(`Bearer `).pop() const suppliedKey = authorizationHeader.split(`Bearer `).pop()
if (suppliedKey) { if (!suppliedKey) return false
const keys = await apikeyService.getAllApiKeys()
const apiSecret = keys.find((key: any) => key.id === chatFlowApiKeyId)?.apiSecret try {
if (!apiSecret) return false const apiKey = await apikeyService.getApiKeyById(chatFlowApiKeyId)
if (!compareKeys(apiSecret, suppliedKey)) return false if (!apiKey) return false
const apiKeyWorkSpaceId = apiKey.workspaceId
if (!apiKeyWorkSpaceId) return false
if (apiKeyWorkSpaceId !== chatflow.workspaceId) return false
const apiSecret = apiKey.apiSecret
if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) return false
return true return true
} } catch (error) {
return false return false
}
} }
/** /**
* Validate API Key * Validate and Get API Key Information
* @param {Request} req * @param {Request} req
* @returns {Promise<{isValid: boolean, apiKey?: ApiKey, workspaceId?: string}>}
*/ */
export const validateAPIKey = async (req: Request) => { export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey; workspaceId?: string }> => {
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
if (!authorizationHeader) return false if (!authorizationHeader) return { isValid: false }
const suppliedKey = authorizationHeader.split(`Bearer `).pop() const suppliedKey = authorizationHeader.split(`Bearer `).pop()
if (!suppliedKey) return { isValid: false }
if (suppliedKey) { try {
const keys = await apikeyService.getAllApiKeys() const apiKey = await apikeyService.getApiKey(suppliedKey)
const apiSecret = keys.find((key: any) => key.apiKey === suppliedKey)?.apiSecret if (!apiKey) return { isValid: false }
if (!apiSecret) return false
if (!compareKeys(apiSecret, suppliedKey)) return false const apiKeyWorkSpaceId = apiKey.workspaceId
return true if (!apiKeyWorkSpaceId) return { isValid: false }
const apiSecret = apiKey.apiSecret
if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) {
return { isValid: false, apiKey, workspaceId: apiKey.workspaceId }
} }
return false
}
/** return { isValid: true, apiKey, workspaceId: apiKey.workspaceId }
* Get API Key WorkspaceID } catch (error) {
* @param {Request} req return { isValid: false }
*/
export const getAPIKeyWorkspaceID = async (req: Request) => {
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
if (!authorizationHeader) return false
const suppliedKey = authorizationHeader.split(`Bearer `).pop()
if (suppliedKey) {
const key = await apikeyService.getApiKey(suppliedKey)
return key?.workspaceId
} }
return undefined
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client' import client from './client'
const getAllAPIKeys = () => client.get('/apikey') const getAllAPIKeys = (params) => client.get('/apikey', { params })
const createNewAPI = (body) => client.post(`/apikey`, body) const createNewAPI = (body) => client.post(`/apikey`, body)
+2 -2
View File
@@ -1,8 +1,8 @@
import client from './client' import client from './client'
const getAllChatflows = () => client.get('/chatflows?type=CHATFLOW') const getAllChatflows = (params) => client.get('/chatflows?type=CHATFLOW', { params })
const getAllAgentflows = (type) => client.get(`/chatflows?type=${type}`) const getAllAgentflows = (type, params) => client.get(`/chatflows?type=${type}`, { params })
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`) const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
+2 -2
View File
@@ -1,9 +1,9 @@
import client from './client' import client from './client'
const getAllDatasets = () => client.get('/datasets') const getAllDatasets = (params) => client.get('/datasets', { params })
//dataset //dataset
const getDataset = (id) => client.get(`/datasets/set/${id}`) const getDataset = (id, params) => client.get(`/datasets/set/${id}`, { params })
const createDataset = (body) => client.post(`/datasets/set`, body) const createDataset = (body) => client.post(`/datasets/set`, body)
const updateDataset = (id, body) => client.put(`/datasets/set/${id}`, body) const updateDataset = (id, body) => client.put(`/datasets/set/${id}`, body)
const deleteDataset = (id) => client.delete(`/datasets/set/${id}`) const deleteDataset = (id) => client.delete(`/datasets/set/${id}`)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client' import client from './client'
const getAllDocumentStores = () => client.get('/document-store/store') const getAllDocumentStores = (params) => client.get('/document-store/store', { params })
const getDocumentLoaders = () => client.get('/document-store/components/loaders') const getDocumentLoaders = () => client.get('/document-store/components/loaders')
const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`) const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`)
const createDocumentStore = (body) => client.post(`/document-store/store`, body) const createDocumentStore = (body) => client.post(`/document-store/store`, body)
+1 -1
View File
@@ -1,7 +1,7 @@
import client from './client' import client from './client'
//evaluation //evaluation
const getAllEvaluations = () => client.get('/evaluations') const getAllEvaluations = (params) => client.get('/evaluations', { params })
const getIsOutdated = (id) => client.get(`/evaluations/is-outdated/${id}`) const getIsOutdated = (id) => client.get(`/evaluations/is-outdated/${id}`)
const getEvaluation = (id) => client.get(`/evaluations/${id}`) const getEvaluation = (id) => client.get(`/evaluations/${id}`)
const createEvaluation = (body) => client.post(`/evaluations`, body) const createEvaluation = (body) => client.post(`/evaluations`, body)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client' import client from './client'
const getAllEvaluators = () => client.get('/evaluators') const getAllEvaluators = (params) => client.get('/evaluators', { params })
//evaluators //evaluators
const createEvaluator = (body) => client.post(`/evaluators`, body) const createEvaluator = (body) => client.post(`/evaluators`, body)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client' import client from './client'
const getAllTools = () => client.get('/tools') const getAllTools = (params) => client.get('/tools', { params })
const getSpecificTool = (id) => client.get(`/tools/${id}`) const getSpecificTool = (id) => client.get(`/tools/${id}`)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client' import client from './client'
const getAllVariables = () => client.get('/variables') const getAllVariables = (params) => client.get('/variables', { params })
const createVariable = (body) => client.post(`/variables`, body) const createVariable = (body) => client.post(`/variables`, body)
@@ -8,8 +8,14 @@ import Typography from '@mui/material/Typography'
const StatsCard = ({ title, stat }) => { const StatsCard = ({ title, stat }) => {
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
return ( return (
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}> <Card
<CardContent> sx={{
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
boxShadow: customization.isDarkMode ? '0px 3px 8px rgba(255, 255, 255, 0.5)' : 'none',
borderRadius: `${customization.borderRadius}px`
}}
>
<CardContent sx={{ padding: '12px', '&:last-child': { paddingBottom: '12px', paddingLeft: '18px', paddingRight: '8px' } }}>
<Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom> <Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom>
{title} {title}
</Typography> </Typography>
@@ -24,9 +24,14 @@ import {
CardContent, CardContent,
FormControlLabel, FormControlLabel,
Checkbox, Checkbox,
DialogActions DialogActions,
Pagination,
Typography,
Menu,
MenuItem,
IconButton
} from '@mui/material' } from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme, styled, alpha } from '@mui/material/styles'
import DatePicker from 'react-datepicker' import DatePicker from 'react-datepicker'
import robotPNG from '@/assets/images/robot.png' import robotPNG from '@/assets/images/robot.png'
@@ -34,7 +39,8 @@ import userPNG from '@/assets/images/account.png'
import msgEmptySVG from '@/assets/images/message_empty.svg' import msgEmptySVG from '@/assets/images/message_empty.svg'
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png' import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png' import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip } from '@tabler/icons-react' import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip, IconBulb } from '@tabler/icons-react'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
// Project import // Project import
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
@@ -63,6 +69,42 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import '@/views/chatmessage/ChatMessage.css' import '@/views/chatmessage/ChatMessage.css'
import 'react-datepicker/dist/react-datepicker.css' import 'react-datepicker/dist/react-datepicker.css'
const StyledMenu = styled((props) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...props}
/>
))(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 6,
marginTop: theme.spacing(1),
minWidth: 180,
boxShadow:
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
'& .MuiMenu-list': {
padding: '4px 0'
},
'& .MuiMenuItem-root': {
'& .MuiSvgIcon-root': {
fontSize: 18,
color: theme.palette.text.secondary,
marginRight: theme.spacing(1.5)
},
'&:active': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
}
}
}
}))
const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) {
return ( return (
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}> <ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
@@ -104,10 +146,12 @@ const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm })
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<span style={{ marginTop: '20px', marginBottom: '20px' }}>{dialogProps.description}</span> <span style={{ marginTop: '20px', marginBottom: '20px' }}>{dialogProps.description}</span>
{dialogProps.isChatflow && (
<FormControlLabel <FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />} control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node' label='Remove messages from 3rd party Memory Node'
/> />
)}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button> <Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
@@ -142,7 +186,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [chatlogs, setChatLogs] = useState([]) const [chatlogs, setChatLogs] = useState([])
const [allChatlogs, setAllChatLogs] = useState([]) const [allChatlogs, setAllChatLogs] = useState([])
const [chatMessages, setChatMessages] = useState([]) const [chatMessages, setChatMessages] = useState([])
const [stats, setStats] = useState([]) const [stats, setStats] = useState({})
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0) const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
const [selectedChatId, setSelectedChatId] = useState('') const [selectedChatId, setSelectedChatId] = useState('')
const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
@@ -154,6 +198,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [startDate, setStartDate] = useState(new Date(new Date().setMonth(new Date().getMonth() - 1))) const [startDate, setStartDate] = useState(new Date(new Date().setMonth(new Date().getMonth() - 1)))
const [endDate, setEndDate] = useState(new Date()) const [endDate, setEndDate] = useState(new Date())
const [leadEmail, setLeadEmail] = useState('') const [leadEmail, setLeadEmail] = useState('')
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
@@ -161,74 +207,70 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath) const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
let storagePath = '' let storagePath = ''
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(10)
const [total, setTotal] = useState(0)
const onChange = (event, page) => {
setCurrentPage(page)
refresh(page, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
}
const refresh = (page, limit, startDate, endDate, chatTypes, feedbackTypes) => {
getChatmessageApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate,
order: 'DESC',
page: page,
limit: limit
})
getStatsApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate
})
setCurrentPage(page)
}
const onStartDateSelected = (date) => { const onStartDateSelected = (date) => {
const updatedDate = new Date(date) const updatedDate = new Date(date)
updatedDate.setHours(0, 0, 0, 0) updatedDate.setHours(0, 0, 0, 0)
setStartDate(updatedDate) setStartDate(updatedDate)
getChatmessageApi.request(dialogProps.chatflow.id, { refresh(1, pageLimit, updatedDate, endDate, chatTypeFilter, feedbackTypeFilter)
startDate: updatedDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
startDate: updatedDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
} }
const onEndDateSelected = (date) => { const onEndDateSelected = (date) => {
const updatedDate = new Date(date) const updatedDate = new Date(date)
updatedDate.setHours(23, 59, 59, 999) updatedDate.setHours(23, 59, 59, 999)
setEndDate(updatedDate) setEndDate(updatedDate)
getChatmessageApi.request(dialogProps.chatflow.id, { refresh(1, pageLimit, startDate, updatedDate, chatTypeFilter, feedbackTypeFilter)
endDate: updatedDate,
startDate: startDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
endDate: updatedDate,
startDate: startDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
} }
const onChatTypeSelected = (chatTypes) => { const onChatTypeSelected = (chatTypes) => {
setChatTypeFilter(chatTypes) // Parse the JSON string from MultiDropdown back to an array
getChatmessageApi.request(dialogProps.chatflow.id, { let parsedChatTypes = []
chatType: chatTypes.length ? chatTypes : undefined, if (chatTypes && typeof chatTypes === 'string' && chatTypes.startsWith('[') && chatTypes.endsWith(']')) {
startDate: startDate, parsedChatTypes = JSON.parse(chatTypes)
endDate: endDate, } else if (Array.isArray(chatTypes)) {
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined parsedChatTypes = chatTypes
}) }
getStatsApi.request(dialogProps.chatflow.id, { setChatTypeFilter(parsedChatTypes)
chatType: chatTypes.length ? chatTypes : undefined, refresh(1, pageLimit, startDate, endDate, parsedChatTypes, feedbackTypeFilter)
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
} }
const onFeedbackTypeSelected = (feedbackTypes) => { const onFeedbackTypeSelected = (feedbackTypes) => {
setFeedbackTypeFilter(feedbackTypes) // Parse the JSON string from MultiDropdown back to an array
let parsedFeedbackTypes = []
getChatmessageApi.request(dialogProps.chatflow.id, { if (feedbackTypes && typeof feedbackTypes === 'string' && feedbackTypes.startsWith('[') && feedbackTypes.endsWith(']')) {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined, parsedFeedbackTypes = JSON.parse(feedbackTypes)
feedbackType: feedbackTypes.length ? feedbackTypes : undefined, } else if (Array.isArray(feedbackTypes)) {
startDate: startDate, parsedFeedbackTypes = feedbackTypes
endDate: endDate, }
order: 'ASC' setFeedbackTypeFilter(parsedFeedbackTypes)
}) refresh(1, pageLimit, startDate, endDate, chatTypeFilter, parsedFeedbackTypes)
getStatsApi.request(dialogProps.chatflow.id, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate
})
} }
const onDeleteMessages = () => { const onDeleteMessages = () => {
@@ -236,7 +278,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
title: 'Delete Messages', title: 'Delete Messages',
description: 'Are you sure you want to delete messages? This action cannot be undone.', description: 'Are you sure you want to delete messages? This action cannot be undone.',
confirmButtonName: 'Delete', confirmButtonName: 'Delete',
cancelButtonName: 'Cancel' cancelButtonName: 'Cancel',
isChatflow: dialogProps.isChatflow
}) })
setHardDeleteDialogOpen(true) setHardDeleteDialogOpen(true)
} }
@@ -280,18 +323,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
) )
} }
}) })
getChatmessageApi.request(chatflowid, { refresh(1, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(chatflowid, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
enqueueSnackbar({ enqueueSnackbar({
@@ -555,20 +587,42 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
item: allChatMessages[i] item: allChatMessages[i]
} }
} else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) { } else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) {
// Properly identify user and API messages regardless of order
const firstMessage = seen[PK].item
const secondMessage = item
let userContent = ''
let apiContent = ''
// Check both messages and assign based on role, not order
if (firstMessage.role === 'userMessage') {
userContent = `User: ${firstMessage.content}`
} else if (firstMessage.role === 'apiMessage') {
apiContent = `Bot: ${firstMessage.content}`
}
if (secondMessage.role === 'userMessage') {
userContent = `User: ${secondMessage.content}`
} else if (secondMessage.role === 'apiMessage') {
apiContent = `Bot: ${secondMessage.content}`
}
seen[PK] = { seen[PK] = {
counter: 2, counter: 2,
item: { item: {
...seen[PK].item, ...seen[PK].item,
apiContent: apiContent,
seen[PK].item.role === 'apiMessage' ? `Bot: ${seen[PK].item.content}` : `User: ${seen[PK].item.content}`, userContent
userContent: item.role === 'apiMessage' ? `Bot: ${item.content}` : `User: ${item.content}`
} }
} }
filteredChatLogs.push(seen[PK].item) filteredChatLogs.push(seen[PK].item)
} }
} }
setChatLogs(filteredChatLogs)
if (filteredChatLogs.length) return getChatPK(filteredChatLogs[0]) // Sort by date to maintain chronological order
const sortedChatLogs = filteredChatLogs.sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate))
setChatLogs(sortedChatLogs)
if (sortedChatLogs.length) return getChatPK(sortedChatLogs[0])
return undefined return undefined
} }
@@ -613,6 +667,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setSourceDialogOpen(true) setSourceDialogOpen(true)
} }
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const renderFileUploads = (item, index) => { const renderFileUploads = (item, index) => {
if (item?.mime?.startsWith('image/')) { if (item?.mime?.startsWith('image/')) {
return ( return (
@@ -706,15 +768,13 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
useEffect(() => { useEffect(() => {
if (getStatsApi.data) { if (getStatsApi.data) {
setStats(getStatsApi.data) setStats(getStatsApi.data)
setTotal(getStatsApi.data?.totalSessions ?? 0)
} }
}, [getStatsApi.data]) }, [getStatsApi.data])
useEffect(() => { useEffect(() => {
if (dialogProps.chatflow) { if (dialogProps.chatflow) {
getChatmessageApi.request(dialogProps.chatflow.id, { refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
startDate: startDate,
endDate: endDate
})
getStatsApi.request(dialogProps.chatflow.id, { getStatsApi.request(dialogProps.chatflow.id, {
startDate: startDate, startDate: startDate,
endDate: endDate endDate: endDate
@@ -733,6 +793,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setEndDate(new Date()) setEndDate(new Date())
setStats([]) setStats([])
setLeadEmail('') setLeadEmail('')
setTotal(0)
setCurrentPage(1)
setPageLimit(10)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -748,16 +811,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
if (dialogProps.chatflow) { if (dialogProps.chatflow) {
// when the filter is cleared fetch all messages // when the filter is cleared fetch all messages
if (feedbackTypeFilter.length === 0) { if (feedbackTypeFilter.length === 0) {
getChatmessageApi.request(dialogProps.chatflow.id, { refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
startDate: startDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
startDate: startDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -819,19 +873,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
onClose={onCancel} onClose={onCancel}
open={show} open={show}
fullWidth fullWidth
maxWidth={'lg'} maxWidth={'xl'}
aria-labelledby='alert-dialog-title' aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description' aria-describedby='alert-dialog-description'
> >
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }} />
<Button variant='outlined' onClick={() => exportMessages()} startIcon={<IconFileExport />}>
Export
</Button>
</div>
</DialogTitle>
<DialogContent> <DialogContent>
<> <>
<div <div
@@ -912,7 +957,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<b style={{ marginRight: 10 }}>Feedback</b> <b style={{ marginRight: 10 }}>Feedback</b>
<MultiDropdown <MultiDropdown
key={JSON.stringify(feedbackTypeFilter)} key={JSON.stringify(feedbackTypeFilter)}
name='chatType' name='feedbackType'
options={[ options={[
{ {
label: 'Positive', label: 'Positive',
@@ -929,31 +974,81 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
/> />
</div> </div>
<div style={{ flex: 1 }}></div> <div style={{ flex: 1 }}></div>
{stats.totalMessages > 0 && ( <Button
<Button color='error' variant='outlined' onClick={() => onDeleteMessages()} startIcon={<IconEraser />}> id='messages-dialog-action-button'
Delete Messages aria-controls={open ? 'messages-dialog-action-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
variant={customization.isDarkMode ? 'contained' : 'outlined'}
disableElevation
color='secondary'
onClick={handleClick}
sx={{
minWidth: 150,
'&:hover': {
backgroundColor: customization.isDarkMode ? alpha(theme.palette.secondary.main, 0.8) : undefined
}
}}
endIcon={
<KeyboardArrowDownIcon style={{ backgroundColor: customization.isDarkMode ? 'transparent' : 'inherit' }} />
}
>
More Actions
</Button> </Button>
<StyledMenu
id='messages-dialog-action-menu'
MenuListProps={{
'aria-labelledby': 'messages-dialog-action-button'
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => {
handleClose()
exportMessages()
}}
disableRipple
>
<IconFileExport style={{ marginRight: 8 }} />
Export to JSON
</MenuItem>
{(stats.totalMessages ?? 0) > 0 && (
<MenuItem
onClick={() => {
handleClose()
onDeleteMessages()
}}
disableRipple
>
<IconEraser style={{ marginRight: 8 }} />
Delete All
</MenuItem>
)} )}
</StyledMenu>
</div> </div>
<div <div
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 10, gap: 10,
marginBottom: 16, marginBottom: 25,
marginLeft: 8, marginLeft: 8,
marginRight: 8 marginRight: 8,
marginTop: 20
}} }}
> >
<StatsCard title='Total Messages' stat={`${stats.totalMessages}`} /> <StatsCard title='Total Sessions' stat={`${stats.totalSessions ?? 0}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback}`} /> <StatsCard title='Total Messages' stat={`${stats.totalMessages ?? 0}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback ?? 0}`} />
<StatsCard <StatsCard
title='Positive Feedback' title='Positive Feedback'
stat={`${((stats.positiveFeedback / stats.totalFeedback) * 100 || 0).toFixed(2)}%`} stat={`${(((stats.positiveFeedback ?? 0) / (stats.totalFeedback ?? 1)) * 100 || 0).toFixed(2)}%`}
/> />
</div> </div>
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row', overflow: 'hidden', minWidth: 0 }}>
{chatlogs && chatlogs.length == 0 && ( {chatlogs && chatlogs.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}> <Box sx={{ p: 5, height: 'auto' }}>
<img <img
@@ -966,7 +1061,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</Stack> </Stack>
)} )}
{chatlogs && chatlogs.length > 0 && ( {chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '40%' }}> <div style={{ flexBasis: '40%', minWidth: 0, overflow: 'hidden' }}>
<Box <Box
sx={{ sx={{
overflowY: 'auto', overflowY: 'auto',
@@ -976,6 +1071,28 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
maxHeight: 'calc(100vh - 260px)' maxHeight: 'calc(100vh - 260px)'
}} }}
> >
<div
style={{
display: 'flex',
marginLeft: '15px',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10
}}
>
<Typography variant='h5'>
Sessions {pageLimit * (currentPage - 1) + 1} - {Math.min(pageLimit * currentPage, total)} of{' '}
{total}
</Typography>
<Pagination
style={{ justifyItems: 'right', justifyContent: 'center' }}
count={Math.ceil(total / pageLimit)}
onChange={onChange}
page={currentPage}
color='primary'
/>
</div>
{chatlogs.map((chatmsg, index) => ( {chatlogs.map((chatmsg, index) => (
<ListItemButton <ListItemButton
key={index} key={index}
@@ -1018,9 +1135,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</div> </div>
)} )}
{chatlogs && chatlogs.length > 0 && ( {chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '60%', paddingRight: '30px' }}> <div style={{ flexBasis: '60%', paddingRight: '30px', minWidth: 0, overflow: 'hidden' }}>
{chatMessages && chatMessages.length > 1 && ( {chatMessages && chatMessages.length > 1 && (
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div style={{ flex: 1, marginLeft: '20px', marginBottom: '15px', marginTop: '10px' }}> <div style={{ flex: 1, marginLeft: '20px', marginBottom: '15px', marginTop: '10px' }}>
{chatMessages[1].sessionId && ( {chatMessages[1].sessionId && (
<div> <div>
@@ -1046,31 +1163,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'row',
alignContent: 'center', alignContent: 'center',
alignItems: 'end' alignItems: 'end'
}} }}
> >
<StyledButton <Tooltip title='Clear Message'>
sx={{ height: 'max-content', width: 'max-content' }} <IconButton color='error' onClick={() => clearChat(chatMessages[1])}>
variant='outlined' <IconEraser />
color='error' </IconButton>
title='Clear Message' </Tooltip>
onClick={() => clearChat(chatMessages[1])}
startIcon={<IconEraser />}
>
Clear
</StyledButton>
{chatMessages[1].sessionId && ( {chatMessages[1].sessionId && (
<Tooltip <Tooltip
title={ title={
'At your left 👈 you will see the Memory node that was used in this conversation. You need to have the matching Memory node with same parameters in the canvas, in order to delete the session conversations stored on the Memory node' 'On the left 👈, youll see the Memory node used in this conversation. To delete the session conversations stored on that Memory node, you must have a matching Memory node with identical parameters in the canvas.'
} }
placement='bottom' placement='bottom'
> >
<h5 style={{ cursor: 'pointer', color: theme.palette.primary.main }}> <IconButton color='primary'>
Why my session is not deleted? <IconBulb />
</h5> </IconButton>
</Tooltip> </Tooltip>
)} )}
</div> </div>
@@ -1081,12 +1193,15 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
marginLeft: '20px', marginLeft: '20px',
border: '1px solid #e0e0e0', marginBottom: '5px',
borderRadius: `${customization.borderRadius}px` border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
boxShadow: customization.isDarkMode ? '0 0 5px 0 rgba(255, 255, 255, 0.5)' : 'none',
borderRadius: `10px`,
overflow: 'hidden'
}} }}
className='cloud-message' className='cloud-message'
> >
<div style={{ width: '100%', height: '100%' }}> <div style={{ width: '100%', height: '100%', overflowY: 'auto' }}>
{chatMessages && {chatMessages &&
chatMessages.map((message, index) => { chatMessages.map((message, index) => {
if (message.type === 'apiMessage' || message.type === 'userMessage') { if (message.type === 'apiMessage' || message.type === 'userMessage') {
@@ -1125,7 +1240,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%' width: '100%',
minWidth: 0,
overflow: 'hidden'
}} }}
> >
{message.fileUploads && message.fileUploads.length > 0 && ( {message.fileUploads && message.fileUploads.length > 0 && (
@@ -1412,7 +1529,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
})} })}
</div> </div>
)} )}
<div className='markdownanswer'> <div
className='markdownanswer'
style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}
>
<MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}> <MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}>
{message.message} {message.message}
</MemoizedReactMarkdown> </MemoizedReactMarkdown>
@@ -1486,7 +1606,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
return ( return (
<Box <Box
sx={{ sx={{
background: theme.palette.timeMessage.main, background: customization.isDarkMode
? theme.palette.divider
: theme.palette.timeMessage.main,
p: 2 p: 2
}} }}
key={index} key={index}
@@ -0,0 +1,85 @@
import { Box, FormControl, MenuItem, Pagination, Select, Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import { useTheme } from '@mui/material/styles'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
export const DEFAULT_ITEMS_PER_PAGE = 12
const TablePagination = ({ currentPage, limit, total, onChange }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const borderColor = theme.palette.grey[900] + 25
const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE)
const [activePage, setActivePage] = useState(1)
const [totalItems, setTotalItems] = useState(0)
useEffect(() => {
setTotalItems(total)
}, [total])
useEffect(() => {
setItemsPerPage(limit)
}, [limit])
useEffect(() => {
setActivePage(currentPage)
}, [currentPage])
const handlePageChange = (event, value) => {
setActivePage(value)
onChange(value, itemsPerPage)
}
const handleLimitChange = (event) => {
const itemsPerPage = parseInt(event.target.value, 10)
setItemsPerPage(itemsPerPage)
setActivePage(1)
onChange(1, itemsPerPage)
}
return (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant='body2'>Items per page:</Typography>
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
}}
>
<Select value={itemsPerPage} onChange={handleLimitChange} displayEmpty>
<MenuItem value={12}>12</MenuItem>
<MenuItem value={24}>24</MenuItem>
<MenuItem value={48}>48</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Box>
{totalItems > 0 && (
<Typography variant='body2'>
Items {activePage * itemsPerPage - itemsPerPage + 1} to{' '}
{activePage * itemsPerPage > totalItems ? totalItems : activePage * itemsPerPage} of {totalItems}
</Typography>
)}
<Pagination count={Math.ceil(totalItems / itemsPerPage)} onChange={handlePageChange} page={activePage} color='primary' />
</Box>
)
}
TablePagination.propTypes = {
onChange: PropTypes.func.isRequired,
currentPage: PropTypes.number,
limit: PropTypes.number,
total: PropTypes.number
}
export default TablePagination
@@ -0,0 +1,255 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { styled } from '@mui/material/styles'
import {
Box,
Paper,
Skeleton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TableSortLabel,
useTheme,
Typography
} from '@mui/material'
import { tableCellClasses } from '@mui/material/TableCell'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
[`&.${tableCellClasses.head}`]: {
color: theme.palette.grey[900]
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
height: 64
}
}))
const StyledTableRow = styled(TableRow)(() => ({
// hide last border
'&:last-child td, &:last-child th': {
border: 0
}
}))
export const DocumentStoreTable = ({ data, isLoading, onRowClick, images }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const localStorageKeyOrder = 'doc_store_order'
const localStorageKeyOrderBy = 'doc_store_orderBy'
const [order, setOrder] = useState(localStorage.getItem(localStorageKeyOrder) || 'desc')
const [orderBy, setOrderBy] = useState(localStorage.getItem(localStorageKeyOrderBy) || 'name')
const handleRequestSort = (property) => {
const isAsc = orderBy === property && order === 'asc'
const newOrder = isAsc ? 'desc' : 'asc'
setOrder(newOrder)
setOrderBy(property)
localStorage.setItem(localStorageKeyOrder, newOrder)
localStorage.setItem(localStorageKeyOrderBy, property)
}
const sortedData = data
? [...data].sort((a, b) => {
if (orderBy === 'name') {
return order === 'asc' ? (a.name || '').localeCompare(b.name || '') : (b.name || '').localeCompare(a.name || '')
}
return 0
})
: []
return (
<>
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table sx={{ minWidth: 650 }} size='small' aria-label='document_store_table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>&nbsp;</StyledTableCell>
<StyledTableCell>
<TableSortLabel active={orderBy === 'name'} direction={order} onClick={() => handleRequestSort('name')}>
Name
</TableSortLabel>
</StyledTableCell>
<StyledTableCell>Description</StyledTableCell>
<StyledTableCell>Connected flows</StyledTableCell>
<StyledTableCell>Total characters</StyledTableCell>
<StyledTableCell>Total chunks</StyledTableCell>
<StyledTableCell>Loader Types</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{sortedData.map((row, index) => {
return (
<StyledTableRow
onClick={() => onRowClick(row)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<StyledTableCell>
<DocumentStoreStatus isTableView={true} status={row.status} />
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row.name}
</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row?.description}
</Typography>
</StyledTableCell>
<StyledTableCell>{row.whereUsed?.length ?? 0}</StyledTableCell>
<StyledTableCell>{row.totalChars}</StyledTableCell>
<StyledTableCell>{row.totalChunks}</StyledTableCell>
<StyledTableCell>
{images && images[row.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[row.id]
.slice(0, images[row.id].length > 3 ? 3 : images[row.id].length)
.map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images?.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</StyledTableCell>
</StyledTableRow>
)
})}
</>
)}
</TableBody>
</Table>
</TableContainer>
</>
)
}
DocumentStoreTable.propTypes = {
data: PropTypes.array,
isLoading: PropTypes.bool,
images: PropTypes.object,
onRowClick: PropTypes.func
}
DocumentStoreTable.displayName = 'DocumentStoreTable'
+1 -1
View File
@@ -89,7 +89,7 @@ const sanitizeDocumentStore = (DocumentStore) => {
const sanitizeExecution = (Execution) => { const sanitizeExecution = (Execution) => {
try { try {
return Execution.map((execution) => { return Execution.map((execution) => {
execution.agentflow.workspaceId = undefined if (execution.agentflow) execution.agentflow.workspaceId = undefined
return { ...execution, workspaceId: undefined } return { ...execution, workspaceId: undefined }
}) })
} catch (error) { } catch (error) {
+38 -66
View File
@@ -4,7 +4,6 @@ import 'react-datepicker/dist/react-datepicker.css'
// material-ui // material-ui
import { import {
Pagination,
Box, Box,
Stack, Stack,
TextField, TextField,
@@ -21,7 +20,6 @@ import {
DialogTitle, DialogTitle,
IconButton, IconButton,
Tooltip, Tooltip,
Typography,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
@@ -44,6 +42,7 @@ import { IconTrash } from '@tabler/icons-react'
import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable' import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable'
import { ExecutionDetails } from './ExecutionDetails' import { ExecutionDetails } from './ExecutionDetails'
import { omit } from 'lodash' import { omit } from 'lodash'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// ==============================|| AGENT EXECUTIONS ||============================== // // ==============================|| AGENT EXECUTIONS ||============================== //
@@ -71,11 +70,6 @@ const AgentExecutions = () => {
agentflowId: '', agentflowId: '',
sessionId: '' sessionId: ''
}) })
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
total: 0
})
const handleFilterChange = (field, value) => { const handleFilterChange = (field, value) => {
setFilters({ setFilters({
@@ -94,26 +88,25 @@ const AgentExecutions = () => {
}) })
} }
const handlePageChange = (event, newPage) => { /* Table Pagination */
setPagination({ const [currentPage, setCurrentPage] = useState(1)
...pagination, const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
page: newPage const [total, setTotal] = useState(0)
}) const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
} }
const handleLimitChange = (event) => { const applyFilters = (page, limit) => {
setPagination({
...pagination,
page: 1, // Reset to first page when changing items per page
limit: parseInt(event.target.value, 10)
})
}
const applyFilters = () => {
setLoading(true) setLoading(true)
// Ensure page and limit are numbers, not objects
const pageNum = typeof page === 'number' ? page : currentPage
const limitNum = typeof limit === 'number' ? limit : pageLimit
const params = { const params = {
page: pagination.page, page: pageNum,
limit: pagination.limit limit: limitNum
} }
if (filters.state) params.state = filters.state if (filters.state) params.state = filters.state
@@ -152,7 +145,8 @@ const AgentExecutions = () => {
agentflowId: '', agentflowId: '',
sessionId: '' sessionId: ''
}) })
getAllExecutions.request() setCurrentPage(1)
getAllExecutions.request({ page: 1, limit: pageLimit })
} }
const handleExecutionSelectionChange = (selectedIds) => { const handleExecutionSelectionChange = (selectedIds) => {
@@ -175,7 +169,7 @@ const AgentExecutions = () => {
} }
useEffect(() => { useEffect(() => {
getAllExecutions.request() getAllExecutions.request({ page: 1, limit: DEFAULT_ITEMS_PER_PAGE })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -186,7 +180,7 @@ const AgentExecutions = () => {
const { data, total } = getAllExecutions.data const { data, total } = getAllExecutions.data
if (!Array.isArray(data)) return if (!Array.isArray(data)) return
setExecutions(data) setExecutions(data)
setPagination((prev) => ({ ...prev, total })) setTotal(total)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -201,17 +195,12 @@ const AgentExecutions = () => {
setError(getAllExecutions.error) setError(getAllExecutions.error)
}, [getAllExecutions.error]) }, [getAllExecutions.error])
useEffect(() => {
applyFilters()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination.page, pagination.limit])
useEffect(() => { useEffect(() => {
if (deleteExecutionsApi.data) { if (deleteExecutionsApi.data) {
// Refresh the executions list // Refresh the executions list
getAllExecutions.request({ getAllExecutions.request({
page: pagination.page, page: currentPage,
limit: pagination.limit limit: pageLimit
}) })
setSelectedExecutionIds([]) setSelectedExecutionIds([])
} }
@@ -339,7 +328,12 @@ const AgentExecutions = () => {
</Grid> </Grid>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Stack direction='row' spacing={1}> <Stack direction='row' spacing={1}>
<Button variant='contained' color='primary' onClick={applyFilters} size='small'> <Button
variant='contained'
color='primary'
onClick={() => applyFilters(currentPage, pageLimit)}
size='small'
>
Apply Apply
</Button> </Button>
<Button variant='outlined' onClick={resetFilters} size='small'> <Button variant='outlined' onClick={resetFilters} size='small'>
@@ -366,6 +360,8 @@ const AgentExecutions = () => {
</Grid> </Grid>
</Box> </Box>
{executions?.length > 0 && (
<>
<ExecutionsListTable <ExecutionsListTable
data={executions} data={executions}
isLoading={isLoading} isLoading={isLoading}
@@ -373,44 +369,18 @@ const AgentExecutions = () => {
onExecutionRowClick={(execution) => { onExecutionRowClick={(execution) => {
setOpenDrawer(true) setOpenDrawer(true)
const executionDetails = const executionDetails =
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData typeof execution.executionData === 'string'
? JSON.parse(execution.executionData)
: execution.executionData
setSelectedExecutionData(executionDetails) setSelectedExecutionData(executionDetails)
setSelectedMetadata(omit(execution, ['executionData'])) setSelectedMetadata(omit(execution, ['executionData']))
}} }}
/> />
{/* Pagination and Page Size Controls */} {/* Pagination and Page Size Controls */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}> {!isLoading && total > 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
<Typography variant='body2'>Items per page:</Typography> )}
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
}}
>
<Select value={pagination.limit} onChange={handleLimitChange} displayEmpty>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={1000}>1000</MenuItem>
</Select>
</FormControl>
</Box>
<Pagination
count={Math.ceil(pagination.total / pagination.limit)}
page={pagination.page}
onChange={handlePageChange}
color='primary'
/>
</Box>
<ExecutionDetails <ExecutionDetails
open={openDrawer} open={openDrawer}
@@ -429,6 +399,8 @@ const AgentExecutions = () => {
getExecutionByIdApi.request(executionId) getExecutionByIdApi.request(executionId)
}} }}
/> />
</>
)}
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog <Dialog
+36 -17
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
// material-ui // material-ui
import { Chip, Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material' import { Chip, Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
// project imports // project imports
@@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import ViewHeader from '@/layout/MainLayout/ViewHeader' import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary' import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import chatflowsApi from '@/api/chatflows' import chatflowsApi from '@/api/chatflows'
@@ -45,6 +46,25 @@ const Agentflows = () => {
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card') const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
const [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2') const [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit, agentflowVersion)
}
const refresh = (page, limit, nextView) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params)
}
const handleChange = (event, nextView) => { const handleChange = (event, nextView) => {
if (nextView === null) return if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView) localStorage.setItem('flowDisplayStyle', nextView)
@@ -55,7 +75,7 @@ const Agentflows = () => {
if (nextView === null) return if (nextView === null) return
localStorage.setItem('agentFlowVersion', nextView) localStorage.setItem('agentFlowVersion', nextView)
setAgentflowVersion(nextView) setAgentflowVersion(nextView)
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT') refresh(1, pageLimit, nextView)
} }
const onSearchChange = (event) => { const onSearchChange = (event) => {
@@ -87,7 +107,7 @@ const Agentflows = () => {
} }
useEffect(() => { useEffect(() => {
getAllAgentflows.request(agentflowVersion === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT') refresh(currentPage, pageLimit, agentflowVersion)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -107,7 +127,8 @@ const Agentflows = () => {
useEffect(() => { useEffect(() => {
if (getAllAgentflows.data) { if (getAllAgentflows.data) {
try { try {
const agentflows = getAllAgentflows.data const agentflows = getAllAgentflows.data?.data
setTotal(getAllAgentflows.data?.total)
const images = {} const images = {}
const icons = {} const icons = {}
for (let i = 0; i < agentflows.length; i += 1) { for (let i = 0; i < agentflows.length; i += 1) {
@@ -189,6 +210,7 @@ const Agentflows = () => {
<ToggleButtonGroup <ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }} sx={{ borderRadius: 2, maxHeight: 40 }}
value={view} value={view}
disabled={total === 0}
color='primary' color='primary'
exclusive exclusive
onChange={handleChange} onChange={handleChange}
@@ -228,17 +250,11 @@ const Agentflows = () => {
Add New Add New
</StyledPermissionButton> </StyledPermissionButton>
</ViewHeader> </ViewHeader>
{!view || view === 'card' ? ( {!isLoading && total > 0 && (
<> <>
{isLoading && !getAllAgentflows.data ? ( {!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} /> {getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => (
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAgentflows.data?.filter(filterFlows).map((data, index) => (
<ItemCard <ItemCard
key={index} key={index}
onClick={() => goToCanvas(data)} onClick={() => goToCanvas(data)}
@@ -248,13 +264,11 @@ const Agentflows = () => {
/> />
))} ))}
</Box> </Box>
)}
</>
) : ( ) : (
<FlowListTable <FlowListTable
isAgentCanvas={true} isAgentCanvas={true}
isAgentflowV2={agentflowVersion === 'v2'} isAgentflowV2={agentflowVersion === 'v2'}
data={getAllAgentflows.data} data={getAllAgentflows.data?.data}
images={images} images={images}
icons={icons} icons={icons}
isLoading={isLoading} isLoading={isLoading}
@@ -263,7 +277,12 @@ const Agentflows = () => {
setError={setError} setError={setError}
/> />
)} )}
{!isLoading && (!getAllAgentflows.data || getAllAgentflows.data.length === 0) && ( {/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
{!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
+32 -7
View File
@@ -33,6 +33,8 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary' import ErrorBoundary from '@/ErrorBoundary'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available' import { Available } from '@/ui-component/rbac/available'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import apiKeyApi from '@/api/apikey' import apiKeyApi from '@/api/apikey'
@@ -59,7 +61,6 @@ import {
IconFileUpload IconFileUpload
} from '@tabler/icons-react' } from '@tabler/icons-react'
import APIEmptySVG from '@/assets/images/api_empty.svg' import APIEmptySVG from '@/assets/images/api_empty.svg'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
// ==============================|| APIKey ||============================== // // ==============================|| APIKey ||============================== //
@@ -222,6 +223,26 @@ const APIKey = () => {
const [uploadDialogProps, setUploadDialogProps] = useState({}) const [uploadDialogProps, setUploadDialogProps] = useState({})
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAPIKeysApi.request(params)
}
const onSearchChange = (event) => { const onSearchChange = (event) => {
setSearch(event.target.value) setSearch(event.target.value)
} }
@@ -341,12 +362,11 @@ const APIKey = () => {
const onConfirm = () => { const onConfirm = () => {
setShowDialog(false) setShowDialog(false)
setShowUploadDialog(false) setShowUploadDialog(false)
getAllAPIKeysApi.request() refresh(currentPage, pageLimit)
} }
useEffect(() => { useEffect(() => {
getAllAPIKeysApi.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -356,7 +376,8 @@ const APIKey = () => {
useEffect(() => { useEffect(() => {
if (getAllAPIKeysApi.data) { if (getAllAPIKeysApi.data) {
setAPIKeys(getAllAPIKeysApi.data) setAPIKeys(getAllAPIKeysApi.data?.data)
setTotal(getAllAPIKeysApi.data?.total)
} }
}, [getAllAPIKeysApi.data]) }, [getAllAPIKeysApi.data])
@@ -395,7 +416,7 @@ const APIKey = () => {
Create Key Create Key
</StyledPermissionButton> </StyledPermissionButton>
</ViewHeader> </ViewHeader>
{!isLoading && apiKeys.length <= 0 ? ( {!isLoading && apiKeys?.length <= 0 ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
@@ -407,6 +428,7 @@ const APIKey = () => {
<div>No API Keys Yet</div> <div>No API Keys Yet</div>
</Stack> </Stack>
) : ( ) : (
<>
<TableContainer <TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper} component={Paper}
@@ -479,7 +501,7 @@ const APIKey = () => {
</> </>
) : ( ) : (
<> <>
{apiKeys.filter(filterKeys).map((key, index) => ( {apiKeys?.filter(filterKeys).map((key, index) => (
<APIKeyRow <APIKeyRow
key={index} key={index}
apiKey={key} apiKey={key}
@@ -505,6 +527,9 @@ const APIKey = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)} )}
</Stack> </Stack>
)} )}
@@ -510,7 +510,8 @@ const CustomAssistantConfigurePreview = () => {
} else if (setting === 'viewMessages') { } else if (setting === 'viewMessages') {
setViewMessagesDialogProps({ setViewMessagesDialogProps({
title: 'View Messages', title: 'View Messages',
chatflow: canvas.chatflow chatflow: canvas.chatflow,
isChatflow: false
}) })
setViewMessagesDialogOpen(true) setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') { } else if (setting === 'viewLeads') {
@@ -76,7 +76,8 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
} else if (setting === 'viewMessages') { } else if (setting === 'viewMessages') {
setViewMessagesDialogProps({ setViewMessagesDialogProps({
title: 'View Messages', title: 'View Messages',
chatflow: chatflow chatflow: chatflow,
isChatflow: isAgentflowV2 ? false : true
}) })
setViewMessagesDialogOpen(true) setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') { } else if (setting === 'viewLeads') {
+41 -22
View File
@@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import ViewHeader from '@/layout/MainLayout/ViewHeader' import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary' import ErrorBoundary from '@/ErrorBoundary'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import chatflowsApi from '@/api/chatflows' import chatflowsApi from '@/api/chatflows'
@@ -43,6 +44,25 @@ const Chatflows = () => {
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card') const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
}
const applyFilters = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllChatflowsApi.request(params)
}
const handleChange = (event, nextView) => { const handleChange = (event, nextView) => {
if (nextView === null) return if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView) localStorage.setItem('flowDisplayStyle', nextView)
@@ -70,7 +90,7 @@ const Chatflows = () => {
} }
useEffect(() => { useEffect(() => {
getAllChatflowsApi.request() applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -81,7 +101,9 @@ const Chatflows = () => {
useEffect(() => { useEffect(() => {
if (getAllChatflowsApi.data) { if (getAllChatflowsApi.data) {
try { try {
const chatflows = getAllChatflowsApi.data const chatflows = getAllChatflowsApi.data?.data
const total = getAllChatflowsApi.data?.total
setTotal(total)
const images = {} const images = {}
for (let i = 0; i < chatflows.length; i += 1) { for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData const flowDataStr = chatflows[i].flowData
@@ -123,6 +145,7 @@ const Chatflows = () => {
sx={{ borderRadius: 2, maxHeight: 40 }} sx={{ borderRadius: 2, maxHeight: 40 }}
value={view} value={view}
color='primary' color='primary'
disabled={total === 0}
exclusive exclusive
onChange={handleChange} onChange={handleChange}
> >
@@ -161,33 +184,25 @@ const Chatflows = () => {
Add New Add New
</StyledPermissionButton> </StyledPermissionButton>
</ViewHeader> </ViewHeader>
{!view || view === 'card' ? (
<> {isLoading && (
{isLoading && !getAllChatflowsApi.data ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} /> <Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} /> <Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} /> <Skeleton variant='rounded' height={160} />
</Box> </Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data &&
getAllChatflowsApi.data
?.filter(filterFlows)
.map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
data={data}
images={images[data.id]}
/>
))}
</Box>
)} )}
</> {!isLoading && total > 0 && (
<>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => (
<ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
))}
</Box>
) : ( ) : (
<FlowListTable <FlowListTable
data={getAllChatflowsApi.data} data={getAllChatflowsApi.data?.data}
images={images} images={images}
isLoading={isLoading} isLoading={isLoading}
filterFunction={filterFlows} filterFunction={filterFlows}
@@ -195,7 +210,11 @@ const Chatflows = () => {
setError={setError} setError={setError}
/> />
)} )}
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( {/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
{!isLoading && (!getAllChatflowsApi.data?.data || getAllChatflowsApi.data?.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
@@ -26,9 +26,11 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditDatasetRowDialog from './AddEditDatasetRowDialog' import AddEditDatasetRowDialog from './AddEditDatasetRowDialog'
import UploadCSVFileDialog from '@/views/datasets/UploadCSVFileDialog' import UploadCSVFileDialog from '@/views/datasets/UploadCSVFileDialog'
import ErrorBoundary from '@/ErrorBoundary' import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import ViewHeader from '@/layout/MainLayout/ViewHeader' import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import AddEditDatasetDialog from '@/views/datasets/AddEditDatasetDialog' import AddEditDatasetDialog from '@/views/datasets/AddEditDatasetDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import datasetsApi from '@/api/dataset' import datasetsApi from '@/api/dataset'
@@ -45,8 +47,6 @@ import empty_datasetSVG from '@/assets/images/empty_datasets.svg'
import { IconTrash, IconPlus, IconX, IconUpload, IconArrowsDownUp } from '@tabler/icons-react' import { IconTrash, IconPlus, IconX, IconUpload, IconArrowsDownUp } from '@tabler/icons-react'
import DragIndicatorIcon from '@mui/icons-material/DragIndicator' import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Dataset Items ||============================== // // ==============================|| Dataset Items ||============================== //
const EvalDatasetRows = () => { const EvalDatasetRows = () => {
@@ -85,6 +85,25 @@ const EvalDatasetRows = () => {
const [startDragPos, setStartDragPos] = useState(-1) const [startDragPos, setStartDragPos] = useState(-1)
const [endDragPos, setEndDragPos] = useState(-1) const [endDragPos, setEndDragPos] = useState(-1)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getDatasetRows.request(datasetId, params)
}
const handleDragStart = (e, position) => { const handleDragStart = (e, position) => {
draggingItem.current = position draggingItem.current = position
setStartDragPos(position) setStartDragPos(position)
@@ -242,11 +261,11 @@ const EvalDatasetRows = () => {
setShowRowDialog(false) setShowRowDialog(false)
setShowUploadDialog(false) setShowUploadDialog(false)
setShowDatasetDialog(false) setShowDatasetDialog(false)
getDatasetRows.request(datasetId) refresh(currentPage, pageLimit)
} }
useEffect(() => { useEffect(() => {
getDatasetRows.request(datasetId) refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -254,6 +273,7 @@ const EvalDatasetRows = () => {
if (getDatasetRows.data) { if (getDatasetRows.data) {
const dataset = getDatasetRows.data const dataset = getDatasetRows.data
setDataset(dataset) setDataset(dataset)
setTotal(dataset.total)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDatasetRows.data]) }, [getDatasetRows.data])
@@ -449,9 +469,11 @@ const EvalDatasetRows = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
<Typography sx={{ color: theme.palette.grey[600] }} variant='subtitle2'> <Typography sx={{ color: theme.palette.grey[600], marginTop: -2 }} variant='subtitle2'>
<i>Use the drag icon at (extreme right) to reorder the dataset items</i> <i>Use the drag icon at (extreme right) to reorder the dataset items</i>
</Typography> </Typography>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment> </React.Fragment>
)} )}
</Stack> </Stack>
+34 -5
View File
@@ -29,6 +29,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles' import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available' import { Available } from '@/ui-component/rbac/available'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
@@ -70,8 +71,27 @@ const EvalDatasets = () => {
const [datasetDialogProps, setDatasetDialogProps] = useState({}) const [datasetDialogProps, setDatasetDialogProps] = useState({})
const getAllDatasets = useApi(datasetsApi.getAllDatasets) const getAllDatasets = useApi(datasetsApi.getAllDatasets)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDatasets.request(params)
}
const goToRows = (selectedDataset) => { const goToRows = (selectedDataset) => {
navigate(`/dataset_rows/${selectedDataset.id}`) navigate(`/dataset_rows/${selectedDataset.id}?page=1&limit=10`)
} }
const onSearchChange = (event) => { const onSearchChange = (event) => {
@@ -149,7 +169,7 @@ const EvalDatasets = () => {
const onConfirm = () => { const onConfirm = () => {
setShowDatasetDialog(false) setShowDatasetDialog(false)
getAllDatasets.request() refresh()
} }
function filterDatasets(data) { function filterDatasets(data) {
@@ -157,13 +177,14 @@ const EvalDatasets = () => {
} }
useEffect(() => { useEffect(() => {
getAllDatasets.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
if (getAllDatasets.data) { if (getAllDatasets.data) {
setDatasets(getAllDatasets.data) setDatasets(getAllDatasets.data?.data)
setTotal(getAllDatasets.data?.total)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllDatasets.data]) }, [getAllDatasets.data])
@@ -209,6 +230,7 @@ const EvalDatasets = () => {
<div>No Datasets Yet</div> <div>No Datasets Yet</div>
</Stack> </Stack>
) : ( ) : (
<>
<TableContainer <TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper} component={Paper}
@@ -309,7 +331,11 @@ const EvalDatasets = () => {
</Available> </Available>
<Available permission={'datasets:delete'}> <Available permission={'datasets:delete'}>
<TableCell> <TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteDataset(ds)}> <IconButton
title='Delete'
color='error'
onClick={() => deleteDataset(ds)}
>
<IconTrash /> <IconTrash />
</IconButton> </IconButton>
</TableCell> </TableCell>
@@ -321,6 +347,9 @@ const EvalDatasets = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)} )}
</Stack> </Stack>
)} )}
+72 -172
View File
@@ -1,32 +1,18 @@
import { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui // material-ui
import { import { Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
Box,
Paper,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
// project imports // project imports
import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import DocumentStoreCard from '@/ui-component/cards/DocumentStoreCard' import DocumentStoreCard from '@/ui-component/cards/DocumentStoreCard'
import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog' import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog'
import ErrorBoundary from '@/ErrorBoundary'
import ViewHeader from '@/layout/MainLayout/ViewHeader' import ViewHeader from '@/layout/MainLayout/ViewHeader'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
// API // API
@@ -39,13 +25,12 @@ import doc_store_empty from '@/assets/images/doc_store_empty.svg'
// const // const
import { baseURL, gridSpacing } from '@/store/constant' import { baseURL, gridSpacing } from '@/store/constant'
import { useError } from '@/store/context/ErrorContext' import { DocumentStoreTable } from '@/ui-component/table/DocumentStoreTable'
// ==============================|| DOCUMENTS ||============================== // // ==============================|| DOCUMENTS ||============================== //
const Documents = () => { const Documents = () => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization)
const navigate = useNavigate() const navigate = useNavigate()
const getAllDocumentStores = useApi(documentsApi.getAllDocumentStores) const getAllDocumentStores = useApi(documentsApi.getAllDocumentStores)
@@ -66,7 +51,9 @@ const Documents = () => {
} }
function filterDocStores(data) { function filterDocStores(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 return (
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || data.description.toLowerCase().indexOf(search.toLowerCase()) > -1
)
} }
const onSearchChange = (event) => { const onSearchChange = (event) => {
@@ -90,41 +77,61 @@ const Documents = () => {
const onConfirm = () => { const onConfirm = () => {
setShowDialog(false) setShowDialog(false)
getAllDocumentStores.request() applyFilters(currentPage, pageLimit)
} }
useEffect(() => { useEffect(() => {
getAllDocumentStores.request() applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
}
const applyFilters = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDocumentStores.request(params)
}
useEffect(() => { useEffect(() => {
if (getAllDocumentStores.data) { if (getAllDocumentStores.data) {
try { try {
const docStores = getAllDocumentStores.data const { data, total } = getAllDocumentStores.data
if (!Array.isArray(docStores)) return if (!Array.isArray(data)) return
const loaderImages = {} const loaderImages = {}
for (let i = 0; i < docStores.length; i += 1) { for (let i = 0; i < data.length; i += 1) {
const loaders = docStores[i].loaders ?? [] const loaders = data[i].loaders ?? []
let totalChunks = 0 let totalChunks = 0
let totalChars = 0 let totalChars = 0
loaderImages[docStores[i].id] = [] loaderImages[data[i].id] = []
for (let j = 0; j < loaders.length; j += 1) { for (let j = 0; j < loaders.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${loaders[j].loaderId}` const imageSrc = `${baseURL}/api/v1/node-icon/${loaders[j].loaderId}`
if (!loaderImages[docStores[i].id].includes(imageSrc)) { if (!loaderImages[data[i].id].includes(imageSrc)) {
loaderImages[docStores[i].id].push(imageSrc) loaderImages[data[i].id].push(imageSrc)
} }
totalChunks += loaders[j]?.totalChunks ?? 0 totalChunks += loaders[j]?.totalChunks ?? 0
totalChars += loaders[j]?.totalChars ?? 0 totalChars += loaders[j]?.totalChars ?? 0
} }
docStores[i].totalDocs = loaders?.length ?? 0 data[i].totalDocs = loaders?.length ?? 0
docStores[i].totalChunks = totalChunks data[i].totalChunks = totalChunks
docStores[i].totalChars = totalChars data[i].totalChars = totalChars
} }
setDocStores(docStores) setDocStores(data)
setTotal(total)
setImages(loaderImages) setImages(loaderImages)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -136,6 +143,8 @@ const Documents = () => {
setLoading(getAllDocumentStores.loading) setLoading(getAllDocumentStores.loading)
}, [getAllDocumentStores.loading]) }, [getAllDocumentStores.loading])
const hasDocStores = docStores && docStores.length > 0
return ( return (
<MainCard> <MainCard>
{error ? ( {error ? (
@@ -144,11 +153,12 @@ const Documents = () => {
<Stack flexDirection='column' sx={{ gap: 3 }}> <Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader <ViewHeader
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
search={true} search={hasDocStores}
searchPlaceholder='Search Name' searchPlaceholder='Search Name'
title='Document Store' title='Document Store'
description='Store and upsert documents for LLM retrieval (RAG)' description='Store and upsert documents for LLM retrieval (RAG)'
> >
{hasDocStores && (
<ToggleButtonGroup <ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }} sx={{ borderRadius: 2, maxHeight: 40 }}
value={view} value={view}
@@ -181,6 +191,7 @@ const Documents = () => {
<IconList /> <IconList />
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
)}
<StyledPermissionButton <StyledPermissionButton
permissionId={'documentStores:create'} permissionId={'documentStores:create'}
variant='contained' variant='contained'
@@ -192,142 +203,7 @@ const Documents = () => {
Add New Add New
</StyledPermissionButton> </StyledPermissionButton>
</ViewHeader> </ViewHeader>
{!view || view === 'card' ? ( {!hasDocStores ? (
<>
{isLoading && !docStores ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
)}
</>
) : (
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table aria-label='documents table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>&nbsp;</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Connected flows</TableCell>
<TableCell>Total characters</TableCell>
<TableCell>Total chunks</TableCell>
<TableCell>Loader types</TableCell>
</TableRow>
</TableHead>
<TableBody>
{docStores?.filter(filterDocStores).map((data, index) => (
<TableRow
onClick={() => goToDocumentStore(data.id)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align='center'>
<DocumentStoreStatus isTableView={true} status={data.status} />
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.name}
</Typography>
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data?.description}
</Typography>
</TableCell>
<TableCell>{data.whereUsed?.length ?? 0}</TableCell>
<TableCell>{data.totalChars}</TableCell>
<TableCell>{data.totalChunks}</TableCell>
<TableCell>
{images[data.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[data.id].slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{!isLoading && (!docStores || docStores.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
@@ -338,6 +214,30 @@ const Documents = () => {
</Box> </Box>
<div>No Document Stores Created Yet</div> <div>No Document Stores Created Yet</div>
</Stack> </Stack>
) : (
<React.Fragment>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
) : (
<DocumentStoreTable
isLoading={isLoading}
data={docStores?.filter(filterDocStores)}
images={images}
onRowClick={(row) => goToDocumentStore(row.id)}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment>
)} )}
</Stack> </Stack>
)} )}
+29 -5
View File
@@ -33,6 +33,7 @@ import useApi from '@/hooks/useApi'
// Hooks // Hooks
import useConfirm from '@/hooks/useConfirm' import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
import { useError } from '@/store/context/ErrorContext'
// project // project
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -43,6 +44,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles' import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import CreateEvaluationDialog from '@/views/evaluations/CreateEvaluationDialog' import CreateEvaluationDialog from '@/views/evaluations/CreateEvaluationDialog'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// icons // icons
import { import {
@@ -59,8 +61,6 @@ import {
} from '@tabler/icons-react' } from '@tabler/icons-react'
import empty_evalSVG from '@/assets/images/empty_evals.svg' import empty_evalSVG from '@/assets/images/empty_evals.svg'
import { useError } from '@/store/context/ErrorContext'
const EvalsEvaluation = () => { const EvalsEvaluation = () => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
@@ -83,6 +83,24 @@ const EvalsEvaluation = () => {
const [selected, setSelected] = useState([]) const [selected, setSelected] = useState([])
const [autoRefresh, setAutoRefresh] = useState(false) const [autoRefresh, setAutoRefresh] = useState(false)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluations.request(params)
}
const onSelectAllClick = (event) => { const onSelectAllClick = (event) => {
if (event.target.checked) { if (event.target.checked) {
const newSelected = rows.filter((item) => item?.latestEval).map((n) => n.id) const newSelected = rows.filter((item) => item?.latestEval).map((n) => n.id)
@@ -171,13 +189,14 @@ const EvalsEvaluation = () => {
} }
useEffect(() => { useEffect(() => {
getAllEvaluations.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
if (getAllEvaluations.data) { if (getAllEvaluations.data) {
const evalRows = getAllEvaluations.data const evalRows = getAllEvaluations.data.data
setTotal(getAllEvaluations.data.total)
if (evalRows) { if (evalRows) {
// Prepare the data for the table // Prepare the data for the table
for (let i = 0; i < evalRows.length; i++) { for (let i = 0; i < evalRows.length; i++) {
@@ -244,7 +263,8 @@ const EvalsEvaluation = () => {
}, [createNewEvaluation.error]) }, [createNewEvaluation.error])
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
getAllEvaluations.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluations]) }, [getAllEvaluations])
useEffect(() => { useEffect(() => {
@@ -358,6 +378,7 @@ const EvalsEvaluation = () => {
<div>No Evaluations Yet</div> <div>No Evaluations Yet</div>
</Stack> </Stack>
) : ( ) : (
<>
<TableContainer <TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper} component={Paper}
@@ -463,6 +484,9 @@ const EvalsEvaluation = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)} )}
</Stack> </Stack>
)} )}
+63 -18
View File
@@ -14,6 +14,8 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditEvaluatorDialog from '@/views/evaluators/AddEditEvaluatorDialog' import AddEditEvaluatorDialog from '@/views/evaluators/AddEditEvaluatorDialog'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles' import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { PermissionIconButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { PermissionIconButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import { truncateString } from '@/utils/genericHelper'
// API // API
import evaluatorsApi from '@/api/evaluators' import evaluatorsApi from '@/api/evaluators'
@@ -23,15 +25,14 @@ import moment from 'moment/moment'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
import useConfirm from '@/hooks/useConfirm' import useConfirm from '@/hooks/useConfirm'
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
import { useError } from '@/store/context/ErrorContext'
// icons // icons
import empty_evaluatorSVG from '@/assets/images/empty_evaluators.svg' import empty_evaluatorSVG from '@/assets/images/empty_evaluators.svg'
import { IconTrash, IconPlus, IconJson, IconX, IconNumber123, IconAbc, IconAugmentedReality } from '@tabler/icons-react' import { IconTrash, IconPlus, IconJson, IconX, IconNumber123, IconAbc, IconAugmentedReality } from '@tabler/icons-react'
import { truncateString } from '@/utils/genericHelper'
// const // const
import { evaluators as evaluatorsOptions, numericOperators } from '../evaluators/evaluatorConstant' import { evaluators as evaluatorsOptions, numericOperators } from '../evaluators/evaluatorConstant'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Evaluators ||============================== // // ==============================|| Evaluators ||============================== //
@@ -54,6 +55,24 @@ const Evaluators = () => {
const getAllEvaluators = useApi(evaluatorsApi.getAllEvaluators) const getAllEvaluators = useApi(evaluatorsApi.getAllEvaluators)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluators.request(params)
}
const onSearchChange = (event) => { const onSearchChange = (event) => {
setSearch(event.target.value) setSearch(event.target.value)
} }
@@ -129,7 +148,7 @@ const Evaluators = () => {
const onConfirm = () => { const onConfirm = () => {
setShowEvaluatorDialog(false) setShowEvaluatorDialog(false)
getAllEvaluators.request() refresh(currentPage, pageLimit)
} }
function filterDatasets(data) { function filterDatasets(data) {
@@ -137,13 +156,14 @@ const Evaluators = () => {
} }
useEffect(() => { useEffect(() => {
getAllEvaluators.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
if (getAllEvaluators.data) { if (getAllEvaluators.data) {
setEvaluators(getAllEvaluators.data) setEvaluators(getAllEvaluators.data.data)
setTotal(getAllEvaluators.data.total)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluators.data]) }, [getAllEvaluators.data])
@@ -189,6 +209,7 @@ const Evaluators = () => {
<div>No Evaluators Yet</div> <div>No Evaluators Yet</div>
</Stack> </Stack>
) : ( ) : (
<>
<TableContainer <TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper} component={Paper}
@@ -255,22 +276,37 @@ const Evaluators = () => {
<StyledTableRow <StyledTableRow
hover hover
key={index} key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }} sx={{
cursor: 'pointer',
'&:last-child td, &:last-child th': { border: 0 }
}}
> >
<TableCell onClick={() => edit(ds)}> <TableCell onClick={() => edit(ds)}>
{ds?.type === 'numeric' && ( {ds?.type === 'numeric' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}> <Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconNumber123 />} label='Numeric' variant='outlined' /> <Chip
icon={<IconNumber123 />}
label='Numeric'
variant='outlined'
/>
</Stack> </Stack>
)} )}
{ds?.type === 'text' && ( {ds?.type === 'text' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}> <Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconAbc />} label='Text Based' variant='outlined' /> <Chip
icon={<IconAbc />}
label='Text Based'
variant='outlined'
/>
</Stack> </Stack>
)} )}
{ds?.type === 'json' && ( {ds?.type === 'json' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}> <Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconJson />} label='JSON Based' variant='outlined' /> <Chip
icon={<IconJson />}
label='JSON Based'
variant='outlined'
/>
</Stack> </Stack>
)} )}
{ds?.type === 'llm' && ( {ds?.type === 'llm' && (
@@ -309,9 +345,11 @@ const Evaluators = () => {
<span> <span>
<b>Measure</b>:{' '} <b>Measure</b>:{' '}
{ {
[...evaluatorsOptions, ...numericOperators].find( [
(item) => item.name === ds?.measure ...evaluatorsOptions,
)?.label ...numericOperators
].find((item) => item.name === ds?.measure)
?.label
} }
</span> </span>
} }
@@ -332,9 +370,11 @@ const Evaluators = () => {
<span> <span>
<b>Operator</b>:{' '} <b>Operator</b>:{' '}
{ {
[...evaluatorsOptions, ...numericOperators].find( [
(item) => item.name === ds?.operator ...evaluatorsOptions,
)?.label ...numericOperators
].find((item) => item.name === ds?.operator)
?.label
} }
</span> </span>
} }
@@ -381,9 +421,11 @@ const Evaluators = () => {
<span> <span>
<b>Operator</b>:{' '} <b>Operator</b>:{' '}
{ {
[...evaluatorsOptions, ...numericOperators].find( [
(item) => item.name === ds?.operator ...evaluatorsOptions,
)?.label ...numericOperators
].find((item) => item.name === ds?.operator)
?.label
} }
</span> </span>
} }
@@ -510,6 +552,9 @@ const Evaluators = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)} )}
</Stack> </Stack>
)} )}
+45 -17
View File
@@ -12,6 +12,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary' import ErrorBoundary from '@/ErrorBoundary'
import { ToolsTable } from '@/ui-component/table/ToolsListTable' import { ToolsTable } from '@/ui-component/table/ToolsListTable'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import toolsApi from '@/api/tools' import toolsApi from '@/api/tools'
@@ -39,6 +40,25 @@ const Tools = () => {
const inputRef = useRef(null) const inputRef = useRef(null)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllToolsApi.request(params)
}
const handleChange = (event, nextView) => { const handleChange = (event, nextView) => {
if (nextView === null) return if (nextView === null) return
localStorage.setItem('toolsDisplayStyle', nextView) localStorage.setItem('toolsDisplayStyle', nextView)
@@ -102,7 +122,7 @@ const Tools = () => {
const onConfirm = () => { const onConfirm = () => {
setShowDialog(false) setShowDialog(false)
getAllToolsApi.request() refresh(currentPage, pageLimit)
} }
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
@@ -117,8 +137,7 @@ const Tools = () => {
} }
useEffect(() => { useEffect(() => {
getAllToolsApi.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -126,6 +145,12 @@ const Tools = () => {
setLoading(getAllToolsApi.loading) setLoading(getAllToolsApi.loading)
}, [getAllToolsApi.loading]) }, [getAllToolsApi.loading])
useEffect(() => {
if (getAllToolsApi.data) {
setTotal(getAllToolsApi.data.total)
}
}, [getAllToolsApi.data])
return ( return (
<> <>
<MainCard> <MainCard>
@@ -144,6 +169,7 @@ const Tools = () => {
sx={{ borderRadius: 2, maxHeight: 40 }} sx={{ borderRadius: 2, maxHeight: 40 }}
value={view} value={view}
color='primary' color='primary'
disabled={total === 0}
exclusive exclusive
onChange={handleChange} onChange={handleChange}
> >
@@ -203,27 +229,29 @@ const Tools = () => {
</StyledPermissionButton> </StyledPermissionButton>
</ButtonGroup> </ButtonGroup>
</ViewHeader> </ViewHeader>
{!view || view === 'card' ? ( {isLoading && (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
)}
{!isLoading && total > 0 && (
<> <>
{isLoading ? ( {!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} /> {getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => (
<Skeleton variant='rounded' height={160} /> <ItemCard data={data} key={index} onClick={() => edit(data)} />
<Skeleton variant='rounded' height={160} /> ))}
</Box> </Box>
) : ( ) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}> <ToolsTable data={getAllToolsApi.data.data} isLoading={isLoading} onSelect={edit} />
{getAllToolsApi.data &&
getAllToolsApi.data
?.filter(filterTools)
.map((data, index) => <ItemCard data={data} key={index} onClick={() => edit(data)} />)}
</Box>
)} )}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</> </>
) : (
<ToolsTable data={getAllToolsApi.data} isLoading={isLoading} onSelect={edit} />
)} )}
{!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( {!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
+33 -5
View File
@@ -33,6 +33,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available' import { Available } from '@/ui-component/rbac/available'
import { refreshVariablesCache } from '@/ui-component/input/suggestionOption' import { refreshVariablesCache } from '@/ui-component/input/suggestionOption'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API // API
import variablesApi from '@/api/variables' import variablesApi from '@/api/variables'
@@ -91,8 +92,27 @@ const Variables = () => {
const { confirm } = useConfirm() const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables) const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllVariables.request(params)
}
const onSearchChange = (event) => { const onSearchChange = (event) => {
setSearch(event.target.value) setSearch(event.target.value)
} }
@@ -172,12 +192,12 @@ const Variables = () => {
const onConfirm = () => { const onConfirm = () => {
setShowVariableDialog(false) setShowVariableDialog(false)
getAllVariables.request() refresh(currentPage, pageLimit)
refreshVariablesCache() refreshVariablesCache()
} }
useEffect(() => { useEffect(() => {
getAllVariables.request() refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
@@ -187,7 +207,8 @@ const Variables = () => {
useEffect(() => { useEffect(() => {
if (getAllVariables.data) { if (getAllVariables.data) {
setVariables(getAllVariables.data) setVariables(getAllVariables.data.data)
setTotal(getAllVariables.data.total)
} }
}, [getAllVariables.data]) }, [getAllVariables.data])
@@ -231,6 +252,7 @@ const Variables = () => {
<div>No Variables Yet</div> <div>No Variables Yet</div>
</Stack> </Stack>
) : ( ) : (
<>
<TableContainer <TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper} component={Paper}
@@ -319,7 +341,10 @@ const Variables = () => {
) : ( ) : (
<> <>
{variables.filter(filterVariables).map((variable, index) => ( {variables.filter(filterVariables).map((variable, index) => (
<StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> <StyledTableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<StyledTableCell component='th' scope='row'> <StyledTableCell component='th' scope='row'>
<div <div
style={{ style={{
@@ -387,6 +412,9 @@ const Variables = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)} )}
</Stack> </Stack>
)} )}