Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
"resolutions": {
"@google/generative-ai": "^0.24.0",
"@grpc/grpc-js": "^1.10.10",
"@langchain/core": "0.3.59",
"@langchain/core": "0.3.61",
"@qdrant/openapi-typescript-fetch": "1.2.6",
"openai": "4.96.0",
"protobufjs": "7.4.0"
Expand Down
1 change: 1 addition & 0 deletions packages-answers/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export interface Message extends Partial<DB.Message> {
role: string
feedbacks?: MessageFeedback[]
content: string
followUpPrompts?: string[]
}

export type AlgoliaHit = Hit<{
Expand Down
48 changes: 40 additions & 8 deletions packages-answers/ui/src/AnswersContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,13 +533,37 @@ export function AnswersProvider({
else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2)
else text = JSON.stringify(data, null, 2)

// Parse followUpPrompts safely - handle double-stringification from backend
let followUpPrompts = undefined
if (data?.followUpPrompts) {
try {
followUpPrompts = data.followUpPrompts
// First parse if it's a string
if (typeof followUpPrompts === 'string') {
followUpPrompts = JSON.parse(followUpPrompts)
}
// Second parse if still a string (backend double-stringifies)
if (typeof followUpPrompts === 'string') {
followUpPrompts = JSON.parse(followUpPrompts)
}
// Ensure it's an array
if (!Array.isArray(followUpPrompts)) {
followUpPrompts = undefined
}
} catch (e) {
console.error('Failed to parse followUpPrompts:', e)
followUpPrompts = undefined
}
}

setMessages((prevMessages) => [
...prevMessages,
{
role: 'assistant',
content: text,
id: data?.chatMessageId,
sourceDocuments: data?.sourceDocuments,
followUpPrompts,
usedTools: data?.usedTools,
fileAnnotations: data?.fileAnnotations,
agentReasoning: data?.agentReasoning,
Expand Down Expand Up @@ -636,17 +660,25 @@ export function AnswersProvider({
if (payload.data.chatId) {
setChatId(payload.data.chatId)
}
if (payload.data.chatMessageId) {
setMessages((prevMessages) => {
const allMessages = [...cloneDeep(prevMessages)]
const lastMessage = allMessages[allMessages.length - 1]
if (lastMessage?.role === 'user') return allMessages
setMessages((prevMessages) => {
const allMessages = [...cloneDeep(prevMessages)]
const lastMessage = allMessages[allMessages.length - 1]
if (lastMessage?.role === 'user') return allMessages

// Update message ID and chat info
if (payload.data.chatMessageId) {
lastMessage.id = payload.data.chatMessageId
lastMessage.chatId = payload.data.chatId
lastMessage.chatflowid = chatflowid
return allMessages
})
}
}

// Add follow-up prompts if present
if (payload.data.followUpPrompts) {
lastMessage.followUpPrompts = payload.data.followUpPrompts
}

return allMessages
})
break
case 'error':
setError(payload.data)
Expand Down
30 changes: 19 additions & 11 deletions packages-answers/ui/src/ChatRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,25 @@ export const ChatRoom: React.FC<ChatRoomProps> = ({
onCancel={() => setShowFeedbackContentDialog(false)}
onConfirm={submitFeedbackContent}
/>
{messages?.map((message, index) => (
<MessageCard
{...message}
key={`message_${index}`}
setSelectedDocuments={setSelectedDocuments}
setPreviewCode={setPreviewCode}
openLinksInNewTab={openLinksInNewTab}
role={message.role}
isFeedbackAllowed={chatbotConfig?.chatFeedback?.status}
/>
))}
{messages?.map((message, index) => {
// Simple check: is this the last message AND is it an assistant message?
// This ensures follow-up prompts only show on the very last assistant response
const isLastMessage = index === messages.length - 1 && message.role === 'assistant'

return (
<MessageCard
{...message}
key={`message_${index}`}
setSelectedDocuments={setSelectedDocuments}
setPreviewCode={setPreviewCode}
openLinksInNewTab={openLinksInNewTab}
role={message.role}
isFeedbackAllowed={chatbotConfig?.chatFeedback?.status}
isLastMessage={isLastMessage}
followUpPrompts={message.followUpPrompts}
/>
)
})}

{error ? (
<>
Expand Down
51 changes: 51 additions & 0 deletions packages-answers/ui/src/FollowUpPrompts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import { Box, Typography, Chip } from '@mui/material'
import { IconSparkles } from '@tabler/icons-react'

interface FollowUpPromptsProps {
prompts: string[]
onPromptClick: (prompt: string) => void
}

export const FollowUpPrompts: React.FC<FollowUpPromptsProps> = ({ prompts, onPromptClick }) => {
// Safety check for prompts
if (!prompts || prompts.length === 0) {
return null
}

return (
<Box sx={{ mt: 2 }}>
{/* Header with sparkles icon and text */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 1 }}>
<IconSparkles size={16} color='#1976D2' />
<Typography variant='body2' sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
Try these prompts
</Typography>
</Box>

{/* Prompts container */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{prompts.map((prompt, index) => (
<Chip
key={index}
label={prompt}
onClick={() => onPromptClick(prompt)}
variant='outlined'
Comment on lines +10 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Safeguard follow-up prompt rendering for stored string values

The new FollowUpPrompts component assumes prompts is an array and immediately calls prompts.map(...), but chat messages persisted in the DB store followUpPrompts as JSON strings (see the server’s buildChatflow.ts where the value is JSON.stringify). When an existing chat session is reloaded, message.followUpPrompts is still a string and FollowUpPrompts will try to execute .map on that string, throwing prompts.map is not a function and breaking the message rendering. Guard the component with Array.isArray(prompts) or deserialize earlier so historic conversations with follow-up prompts don’t crash the chat UI.

Useful? React with 👍 / 👎.

sx={{
cursor: 'pointer',
borderRadius: '15px',
fontSize: '0.875rem',
'&:hover': {
backgroundColor: 'action.hover',
filter: 'brightness(0.9)'
},
'&:active': {
filter: 'brightness(0.75)'
}
}}
/>
))}
</Box>
</Box>
)
}
29 changes: 18 additions & 11 deletions packages-answers/ui/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { FileUpload } from '../types'
import isArray from 'lodash/isArray'
import { SimpleMarkdown } from './SimpleMarkdown'
import { LoadingAnimation } from './LoadingAnimation'
import { FollowUpPrompts } from '../FollowUpPrompts'
const CodeCard = dynamic(() => import('./CodeCard').then((mod) => ({ default: mod.CodeCard })))
const Dialog = dynamic(() => import('@mui/material/Dialog'))
const DialogActions = dynamic(() => import('@mui/material/DialogActions'))
Expand Down Expand Up @@ -68,6 +69,8 @@ interface MessageCardProps extends Partial<Omit<Message, 'content'>>, MessageExt
) => void
isFeedbackAllowed?: boolean
chatflowid?: string
isLastMessage?: boolean
followUpPrompts?: string[]
}

const getLanguageFromClassName = (className: string | undefined) => {
Expand Down Expand Up @@ -136,6 +139,8 @@ export const MessageCard = ({
chatflowid,
id: messageId,
usedTools,
isLastMessage,
followUpPrompts,
...other
}: MessageCardProps) => {
other = { ...other, role, user } as any
Expand Down Expand Up @@ -259,17 +264,6 @@ export const MessageCard = ({
return fileUploads
}, [fileUploads])

// Update the isLastMessage check to use content instead of ID
const isLastMessage = React.useMemo(() => {
if (!messages || !content) return false

// Get the last non-user message
const lastAiMessage = [...messages].reverse().find((msg) => msg.role !== 'userMessage' && msg.role !== 'user')

// Compare content to identify if this is the last message
return lastAiMessage?.content === content
}, [messages, content])

// Modify the effect to detect partial code blocks
React.useEffect(() => {
if (content && !isUserMessage && setPreviewCode && isLastMessage) {
Expand Down Expand Up @@ -707,6 +701,19 @@ export const MessageCard = ({
</Box>
) : null}
</Box>
{/* Follow-up prompts - only show for assistant messages that are the last message */}
{role === 'assistant' && isLastMessage && followUpPrompts && followUpPrompts.length > 0 && (
<FollowUpPrompts
prompts={followUpPrompts}
onPromptClick={(prompt) => {
// Send the follow-up prompt as a new message
sendMessage({
content: prompt,
sidekick
})
}}
/>
)}
{(other as any).action && (
<Box sx={{ mt: 2, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
{(other as any).action.text && (
Expand Down
75 changes: 5 additions & 70 deletions packages-answers/utils/src/extractAllCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,15 @@
import { getCredentialCategory } from './getCredentialCategory'
import { processFlowCredentials } from './processFlowCredentials'

/**
* Extract all credentials (assigned and unassigned) from flow data for QuickSetup mode
* @param {string|object} flowData - Flow data as JSON string or object
* @returns {object} Object containing all credentials info
*/
export const extractAllCredentials = (flowData: string | any) => {
try {
// Parse flow data if it's a string
const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData
const { credentials, hasCredentials } = processFlowCredentials(flowData)

if (!flow.nodes || !Array.isArray(flow.nodes)) {
return { allCredentials: [], hasCredentials: false }
}

const allCredentials: any[] = []
const credentialTypes = new Set<string>()

// Iterate through all nodes
flow.nodes.forEach((node: any) => {
if (node.data && node.data.inputParams) {
// Find all credential input parameters
const credentialParams = node.data.inputParams.filter((param: any) => {
return param.type === 'credential'
})

credentialParams.forEach((credentialParam: any) => {
// Check if credential is assigned
const hasCredential =
node.data.credential ||
(node.data.inputs && node.data.inputs[credentialParam.name]) ||
(node.data.inputs && node.data.inputs['FLOWISE_CREDENTIAL_ID'])

// Extract credential names
const credentialNames = credentialParam.credentialNames || []

credentialNames.forEach((credentialName: string) => {
credentialTypes.add(credentialName)

// Get category information to determine if credential is core
const category = getCredentialCategory(node.data.category, credentialName)

// Credential is required if:
// 1. Category is core (Chat Models, Agents, etc), OR
// 2. Parameter explicitly marked as NOT optional (optional: false)
// Default to optional if not explicitly marked
const isRequired = category.isCore || (credentialParam.optional === false)

const credInfo = {
nodeId: node.id,
nodeName: node.data.name || 'Unknown Node',
nodeCategory: node.data.category || 'Unknown',
nodeType: node.data.type || node.type || 'Unknown',
credentialType: credentialName,
parameterName: credentialParam.name,
label: credentialParam.label || credentialParam.name,
isRequired: isRequired,
isCore: category.isCore,
categoryType: category.type,
categoryDisplayName: category.displayName,
isAssigned: !!hasCredential,
assignedCredentialId: hasCredential || null
}

allCredentials.push(credInfo)
})
})
}
})

return {
allCredentials,
hasCredentials: credentialTypes.size > 0
}
} catch (error) {
console.error('Error in extractAllCredentials:', error)
return { allCredentials: [], hasCredentials: false }
return {
allCredentials: credentials,
hasCredentials
}
}
Loading
Loading