Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
CMD [ "pnpm", "start" ]
16 changes: 12 additions & 4 deletions packages/components/src/followUpPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ export interface FollowUpPromptResult {
export const generateFollowUpPrompts = async (
followUpPromptsConfig: FollowUpPromptConfig,
apiMessageContent: string,
options: ICommonObject
options: ICommonObject,
sessionHistory?: string
): Promise<FollowUpPromptResult | undefined> => {
if (followUpPromptsConfig) {
if (!followUpPromptsConfig.status) return undefined
const providerConfig = followUpPromptsConfig[followUpPromptsConfig.selectedProvider]
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)
}
Comment on lines +35 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In JavaScript, String.prototype.replace() with a string pattern only replaces the first occurrence. If the prompt template contains multiple instances of the {history} or {session_history} placeholders, only the first one will be substituted. It is recommended to use .replaceAll() to ensure all instances are correctly replaced.

Suggested change
let followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replace('{session_history}', sessionHistory)
}
let followUpPromptsPrompt = providerConfig.prompt.replaceAll('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replaceAll('{session_history}', sessionHistory)
}
References
  1. Prioritize code readability and understandability over conciseness to reduce the potential for future errors.


switch (followUpPromptsConfig.selectedProvider) {
case FollowUpPromptProvider.ANTHROPIC: {
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/controllers/chatflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion packages/server/src/services/chatflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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) {
Expand Down
50 changes: 38 additions & 12 deletions packages/server/src/utils/buildChatflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/utils/getChatMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +119 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The pagination logic is inconsistent with the 1-indexed paging used elsewhere in the project (e.g., in chatflowsService and even later in this file at line 198). If page is 1, page * pageSize will skip the first page of results. Additionally, the check if (!page) page = -1 on line 55 would incorrectly disable pagination if page were 0. Assuming 1-indexed pagination, the calculation should be adjusted to (page - 1) * pageSize.

Suggested change
skip: page > -1 && pageSize > -1 ? page * pageSize : undefined,
take: page > -1 && pageSize > -1 ? pageSize : undefined
skip: page > 0 && pageSize > 0 ? (page - 1) * pageSize : undefined,
take: page > 0 && pageSize > 0 ? pageSize : undefined
References
  1. Maintain consistency with existing code patterns, even if they are not considered best practice.

})

return messages
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ui-component/input/Input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 45 additions & 4 deletions packages/ui/src/views/account/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
DialogActions,
DialogContent,
DialogTitle,
IconButton,
InputAdornment,
LinearProgress,
OutlinedInput,
Skeleton,
Expand All @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -809,12 +814,24 @@ const AccountSettings = () => {
<Typography variant='body1'>Old Password</Typography>
<OutlinedInput
id='oldPassword'
type='password'
type={showOldPassword ? 'text' : 'password'}
fullWidth
placeholder='Old Password'
name='oldPassword'
onChange={(e) => setOldPassword(e.target.value)}
value={oldPassword}
endAdornment={
<InputAdornment position='end'>
<IconButton
edge='end'
onClick={() => setShowOldPassword(!showOldPassword)}
onMouseDown={(e) => e.preventDefault()}
aria-label={showOldPassword ? 'Hide password' : 'Show password'}
>
{showOldPassword ? <IconEyeOff size={18} /> : <IconEye size={18} />}
</IconButton>
</InputAdornment>
}
/>
</Box>
<Box
Expand All @@ -828,12 +845,24 @@ const AccountSettings = () => {
<Typography variant='body1'>New Password</Typography>
<OutlinedInput
id='newPassword'
type='password'
type={showNewPassword ? 'text' : 'password'}
fullWidth
placeholder='New Password'
name='newPassword'
onChange={(e) => setNewPassword(e.target.value)}
value={newPassword}
endAdornment={
<InputAdornment position='end'>
<IconButton
edge='end'
onClick={() => setShowNewPassword(!showNewPassword)}
onMouseDown={(e) => e.preventDefault()}
aria-label={showNewPassword ? 'Hide password' : 'Show password'}
>
{showNewPassword ? <IconEyeOff size={18} /> : <IconEye size={18} />}
</IconButton>
</InputAdornment>
}
/>
<Typography variant='caption'>
<i>
Expand All @@ -853,12 +882,24 @@ const AccountSettings = () => {
<Typography variant='body1'>Confirm New Password</Typography>
<OutlinedInput
id='confirmPassword'
type='password'
type={showConfirmPassword ? 'text' : 'password'}
fullWidth
placeholder='Confirm New Password'
name='confirmPassword'
onChange={(e) => setConfirmPassword(e.target.value)}
value={confirmPassword}
endAdornment={
<InputAdornment position='end'>
<IconButton
edge='end'
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e) => e.preventDefault()}
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? <IconEyeOff size={18} /> : <IconEye size={18} />}
</IconButton>
</InputAdornment>
}
/>
</Box>
</Box>
Expand Down
35 changes: 23 additions & 12 deletions packages/ui/src/views/agentflows/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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(() => {
Comment on lines +95 to +96
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Attaching a timeout ID to the window object is generally discouraged in React components. It creates global state that can lead to side effects (such as conflicts if multiple components use the same property name) and makes the component harder to test or reuse. It is better to use a useRef hook to manage the timeout reference within the component's scope.

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
// )
// }
Comment on lines +101 to +108
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Dead code should be removed rather than commented out to maintain a clean and readable codebase. Since the client-side filtering has been replaced by server-side search, this function is no longer needed and can be safely deleted.


const addNew = () => {
if (agentflowVersion === 'v2') {
Expand Down Expand Up @@ -346,7 +358,7 @@ const Agentflows = () => {
<>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => (
{getAllAgentflows.data?.data.map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
Expand All @@ -366,7 +378,6 @@ const Agentflows = () => {
icons={icons}
scheduleStatuses={scheduleStatuses}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
currentPage={currentPage}
Expand Down
Loading