diff --git a/Dockerfile b/Dockerfile index 9f54dc911b8..8432fcc1659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,19 +27,19 @@ ENV NODE_OPTIONS=--max-old-space-size=8192 WORKDIR /usr/src/flowise -# Copy app source -COPY . . +# Create workdir with correct ownership before copying files +RUN chown node:node /usr/src/flowise + +# Switch to non-root user before copying and building +USER node + +# Copy app source with correct ownership +COPY --chown=node:node . . # Install dependencies and build (excluding sdk packages not needed for Docker) RUN pnpm install && \ pnpm build:docker -# Give the node user ownership of the application files -RUN chown -R node:node . - -# Switch to non-root user (node user already exists in node:20-alpine) -USER node - EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file +CMD [ "pnpm", "start" ] diff --git a/packages/components/src/followUpPrompts.ts b/packages/components/src/followUpPrompts.ts index bc3b2e0f942..4e1fc6f6f56 100644 --- a/packages/components/src/followUpPrompts.ts +++ b/packages/components/src/followUpPrompts.ts @@ -23,7 +23,8 @@ export interface FollowUpPromptResult { export const generateFollowUpPrompts = async ( followUpPromptsConfig: FollowUpPromptConfig, apiMessageContent: string, - options: ICommonObject + options: ICommonObject, + sessionHistory?: string ): Promise => { if (followUpPromptsConfig) { if (!followUpPromptsConfig.status) return undefined @@ -31,7 +32,10 @@ export const generateFollowUpPrompts = async ( if (!providerConfig) return undefined const credentialId = providerConfig.credentialId as string const credentialData = await getCredentialData(credentialId ?? '', options) - const followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent) + let followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent) + if (sessionHistory) { + followUpPromptsPrompt = followUpPromptsPrompt.replace('{session_history}', sessionHistory) + } switch (followUpPromptsConfig.selectedProvider) { case FollowUpPromptProvider.ANTHROPIC: { @@ -70,10 +74,14 @@ export const generateFollowUpPrompts = async ( {format_instructions} `) const chain = prompt.pipe(llm).pipe(parser) - const structuredResponse = await chain.invoke({ + const invokeParams: ICommonObject = { history: apiMessageContent, format_instructions: formatInstructions - }) + } + if (sessionHistory) { + invokeParams.session_history = sessionHistory + } + const structuredResponse = await chain.invoke(invokeParams) return structuredResponse as FollowUpPromptResult } case FollowUpPromptProvider.GOOGLE_GENAI: { diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index dcc0c152d88..ee1017db5ae 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -76,12 +76,14 @@ const deleteChatflow = async (req: Request, res: Response, next: NextFunction) = const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => { try { const { page, limit } = getPageAndLimitParams(req) + const search = req.query?.search as string | undefined const apiResponse = await chatflowsService.getAllChatflows( req.query?.type as ChatflowType, req.user?.activeWorkspaceId, page, - limit + limit, + search ) return res.json(apiResponse) } catch (error) { diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index e301ac4ec7b..a643a651ce6 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -163,7 +163,7 @@ const deleteChatflow = async (chatflowId: string, orgId: string, workspaceId: st } } -const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: number = -1, limit: number = -1) => { +const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: number = -1, limit: number = -1, search?: string) => { try { const appServer = getRunningExpressApp() @@ -186,6 +186,15 @@ const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: queryBuilder.andWhere('chat_flow.type = :type', { type: 'CHATFLOW' }) } if (workspaceId) queryBuilder.andWhere('chat_flow.workspaceId = :workspaceId', { workspaceId }) + if (search) { + queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('LOWER(chat_flow.name) LIKE LOWER(:search)', { search: `%${search}%` }) + .orWhere('LOWER(chat_flow.category) LIKE LOWER(:search)', { search: `%${search}%` }) + .orWhere('LOWER(chat_flow.id) LIKE LOWER(:search)', { search: `%${search}%` }) + }) + ) + } const [data, total] = await queryBuilder.getManyAndCount() if (page > 0 && limit > 0) { diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 7ab1a74c0ba..590c5e684af 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -662,12 +662,25 @@ export const executeFlow = async ({ if (agentflow.followUpPrompts) { const followUpPromptsConfig = JSON.parse(agentflow.followUpPrompts) - const generatedFollowUpPrompts = await generateFollowUpPrompts(followUpPromptsConfig, apiMessage.content, { - chatId, - chatflowid: agentflow.id, - appDataSource, - databaseEntities - }) + // Format session history for the prompt + const formattedSessionHistory = chatHistory + .map((msg) => { + const role = msg.type === 'apiMessage' ? 'Assistant' : 'User' + const content = msg.message || msg.content || '' + return `${role}: ${content}` + }) + .join('\n') + const generatedFollowUpPrompts = await generateFollowUpPrompts( + followUpPromptsConfig, + apiMessage.content, + { + chatId, + chatflowid: agentflow.id, + appDataSource, + databaseEntities + }, + formattedSessionHistory + ) if (generatedFollowUpPrompts?.questions) { apiMessage.followUpPrompts = JSON.stringify(generatedFollowUpPrompts.questions) } @@ -875,12 +888,25 @@ export const executeFlow = async ({ if (result?.action) apiMessage.action = typeof result.action === 'string' ? result.action : JSON.stringify(result.action) if (chatflow.followUpPrompts) { const followUpPromptsConfig = JSON.parse(chatflow.followUpPrompts) - const followUpPrompts = await generateFollowUpPrompts(followUpPromptsConfig, apiMessage.content, { - chatId, - chatflowid, - appDataSource, - databaseEntities - }) + // Format session history for the prompt + const formattedSessionHistory = chatHistory + .map((msg) => { + const role = msg.type === 'apiMessage' ? 'Assistant' : 'User' + const content = msg.message || msg.content || '' + return `${role}: ${content}` + }) + .join('\n') + const followUpPrompts = await generateFollowUpPrompts( + followUpPromptsConfig, + apiMessage.content, + { + chatId, + chatflowid, + appDataSource, + databaseEntities + }, + formattedSessionHistory + ) if (followUpPrompts?.questions) { apiMessage.followUpPrompts = JSON.stringify(followUpPrompts.questions) } diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts index 57b835f9e4f..c2708846f80 100644 --- a/packages/server/src/utils/getChatMessage.ts +++ b/packages/server/src/utils/getChatMessage.ts @@ -115,7 +115,9 @@ export const utilGetChatMessage = async ({ }, order: { createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' - } + }, + skip: page > -1 && pageSize > -1 ? page * pageSize : undefined, + take: page > -1 && pageSize > -1 ? pageSize : undefined }) return messages diff --git a/packages/ui/src/ui-component/input/Input.jsx b/packages/ui/src/ui-component/input/Input.jsx index ad2cda68496..f963cecee4e 100644 --- a/packages/ui/src/ui-component/input/Input.jsx +++ b/packages/ui/src/ui-component/input/Input.jsx @@ -17,7 +17,7 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, onBlu const selectionRangeRef = useRef({ start: null, end: null }) const openPopOver = Boolean(anchorEl) - const hasPasswordToggle = (inputParam?.type === 'password' || inputParam?.type === 'url') && !!inputParam?.enablePasswordToggle + const hasPasswordToggle = inputParam?.type === 'password' || inputParam?.type === 'url' const handleClosePopOver = () => { setAnchorEl(null) diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index ae5bc501d18..9203cecd902 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -15,6 +15,8 @@ import { DialogActions, DialogContent, DialogTitle, + IconButton, + InputAdornment, LinearProgress, OutlinedInput, Skeleton, @@ -32,7 +34,7 @@ import SettingsSection from '@/ui-component/form/settings' import PricingDialog from '@/ui-component/subscription/PricingDialog' // Icons -import { IconAlertCircle, IconCreditCard, IconExternalLink, IconSparkles, IconX } from '@tabler/icons-react' +import { IconAlertCircle, IconCreditCard, IconEye, IconEyeOff, IconExternalLink, IconSparkles, IconX } from '@tabler/icons-react' // API import accountApi from '@/api/account.api' @@ -72,6 +74,9 @@ const AccountSettings = () => { const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') + const [showOldPassword, setShowOldPassword] = useState(false) + const [showNewPassword, setShowNewPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [usage, setUsage] = useState(null) const [isBillingLoading, setIsBillingLoading] = useState(false) const [seatsQuantity, setSeatsQuantity] = useState(0) @@ -809,12 +814,24 @@ const AccountSettings = () => { Old Password setOldPassword(e.target.value)} value={oldPassword} + endAdornment={ + + setShowOldPassword(!showOldPassword)} + onMouseDown={(e) => e.preventDefault()} + aria-label={showOldPassword ? 'Hide password' : 'Show password'} + > + {showOldPassword ? : } + + + } /> { New Password setNewPassword(e.target.value)} value={newPassword} + endAdornment={ + + setShowNewPassword(!showNewPassword)} + onMouseDown={(e) => e.preventDefault()} + aria-label={showNewPassword ? 'Hide password' : 'Show password'} + > + {showNewPassword ? : } + + + } /> @@ -853,12 +882,24 @@ const AccountSettings = () => { Confirm New Password setConfirmPassword(e.target.value)} value={confirmPassword} + endAdornment={ + + setShowConfirmPassword(!showConfirmPassword)} + onMouseDown={(e) => e.preventDefault()} + aria-label={showConfirmPassword ? 'Hide password' : 'Show password'} + > + {showConfirmPassword ? : } + + + } /> diff --git a/packages/ui/src/views/agentflows/index.jsx b/packages/ui/src/views/agentflows/index.jsx index b3db8188ca5..0eb318277d2 100644 --- a/packages/ui/src/views/agentflows/index.jsx +++ b/packages/ui/src/views/agentflows/index.jsx @@ -59,14 +59,17 @@ const Agentflows = () => { setCurrentPage(page) setPageLimit(pageLimit) localStorage.setItem('agentFlowPageSize', pageLimit) - refresh(page, pageLimit, agentflowVersion) + refresh(page, pageLimit, agentflowVersion, search) } - const refresh = (page, limit, nextView) => { + const refresh = (page, limit, nextView, searchTerm) => { const params = { page: page || currentPage, limit: limit || pageLimit } + if (searchTerm) { + params.search = searchTerm + } getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params) } @@ -84,16 +87,25 @@ const Agentflows = () => { } const onSearchChange = (event) => { - setSearch(event.target.value) + const searchValue = event.target.value + setSearch(searchValue) + // Reset to page 1 when search changes + setCurrentPage(1) + // Debounce API call + if (window.searchTimeout) clearTimeout(window.searchTimeout) + window.searchTimeout = setTimeout(() => { + refresh(1, pageLimit, agentflowVersion, searchValue) + }, 300) } - function filterFlows(data) { - return ( - data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || - (data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1) || - data.id.toLowerCase().indexOf(search.toLowerCase()) > -1 - ) - } + // Remove client-side filter function - now using server-side search + // function filterFlows(data) { + // return ( + // data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || + // (data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1) || + // data.id.toLowerCase().indexOf(search.toLowerCase()) > -1 + // ) + // } const addNew = () => { if (agentflowVersion === 'v2') { @@ -346,7 +358,7 @@ const Agentflows = () => { <> {!view || view === 'card' ? ( - {getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => ( + {getAllAgentflows.data?.data.map((data, index) => ( goToCanvas(data)} @@ -366,7 +378,6 @@ const Agentflows = () => { icons={icons} scheduleStatuses={scheduleStatuses} isLoading={isLoading} - filterFunction={filterFlows} updateFlowsApi={getAllAgentflows} setError={setError} currentPage={currentPage} diff --git a/packages/ui/src/views/tools/ToolDialog.jsx b/packages/ui/src/views/tools/ToolDialog.jsx index b73b15ec19a..3e50ef489be 100644 --- a/packages/ui/src/views/tools/ToolDialog.jsx +++ b/packages/ui/src/views/tools/ToolDialog.jsx @@ -79,6 +79,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set const [toolName, setToolName] = useState('') const [toolDesc, setToolDesc] = useState('') const [toolIcon, setToolIcon] = useState('') + const [toolIconError, setToolIconError] = useState('') const [toolSchema, setToolSchema] = useState([]) const [toolFunc, setToolFunc] = useState('') const [showHowToDialog, setShowHowToDialog] = useState(false) @@ -97,6 +98,31 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set [] ) + const validateToolIconUrl = (url) => { + if (!url || url.trim() === '') { + setToolIconError('') + return true + } + try { + const urlObj = new URL(url) + if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { + setToolIconError('Tool Icon Source must be a valid http or https URL') + return false + } + setToolIconError('') + return true + } catch (error) { + setToolIconError('Tool Icon Source must be a valid URL') + return false + } + } + + const handleToolIconChange = (e) => { + const value = e.target.value + setToolIcon(value) + validateToolIconUrl(value) + } + const addNewRow = () => { setTimeout(() => { setToolSchema((prevRows) => { @@ -225,6 +251,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set setToolName('') setToolDesc('') setToolIcon('') + setToolIconError('') setToolSchema([]) setToolFunc('') } @@ -277,6 +304,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set } const addNewTool = async () => { + if (!validateToolIconUrl(toolIcon)) { + return + } try { const obj = { name: toolName, @@ -323,6 +353,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set } const saveTool = async () => { + if (!validateToolIconUrl(toolIcon)) { + return + } try { const saveResp = await toolsApi.updateTool(toolId, { name: toolName, @@ -513,8 +546,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg' value={toolIcon} name='toolIcon' - onChange={(e) => setToolIcon(e.target.value)} + onChange={handleToolIconChange} + error={!!toolIconError} /> + {toolIconError && ( + + {toolIconError} + + )}