diff --git a/packages/server/src/services/executions/index.ts b/packages/server/src/services/executions/index.ts
index 6acae3494..f54ee3220 100644
--- a/packages/server/src/services/executions/index.ts
+++ b/packages/server/src/services/executions/index.ts
@@ -72,7 +72,7 @@ const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data:
const queryBuilder = appServer.AppDataSource.getRepository(Execution)
.createQueryBuilder('execution')
.leftJoinAndSelect('execution.agentflow', 'agentflow')
- .orderBy('execution.createdDate', 'DESC')
+ .orderBy('execution.updatedDate', 'DESC')
.skip((page - 1) * limit)
.take(limit)
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 133c1ea43..73ff9c2f0 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -36,6 +36,7 @@
"@uiw/react-codemirror": "^4.21.21",
"axios": "1.7.9",
"clsx": "^1.1.1",
+ "dompurify": "^3.2.6",
"dotenv": "^16.0.0",
"flowise-embed": "latest",
"flowise-embed-react": "latest",
diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
index 77e398b21..aba719726 100644
--- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
+++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
@@ -44,6 +44,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
// Project import
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
+import { SafeHTML } from '@/ui-component/safe/SafeHTML'
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
import { StyledButton } from '@/ui-component/button/StyledButton'
@@ -860,7 +861,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
} else if (item.type === 'html') {
return (
)
} else {
diff --git a/packages/ui/src/ui-component/json/JsonViewer.jsx b/packages/ui/src/ui-component/json/JsonViewer.jsx
index 823478e61..6d6b72bc7 100644
--- a/packages/ui/src/ui-component/json/JsonViewer.jsx
+++ b/packages/ui/src/ui-component/json/JsonViewer.jsx
@@ -3,31 +3,85 @@ import { Box } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import PropTypes from 'prop-types'
-// Syntax highlighting function for JSON
-function syntaxHighlight(json) {
- if (!json) return '' // No JSON from response
-
- json = json.replace(/&/g, '&').replace(//g, '>')
-
- return json.replace(
- // eslint-disable-next-line
- /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
- function (match) {
- let cls = 'number'
- if (/^"/.test(match)) {
- if (/:$/.test(match)) {
- cls = 'key'
- } else {
- cls = 'string'
- }
- } else if (/true|false/.test(match)) {
- cls = 'boolean'
- } else if (/null/.test(match)) {
- cls = 'null'
- }
- return '' + match + ''
+const JsonToken = ({ type, children, isDarkMode }) => {
+ const getTokenStyle = (tokenType) => {
+ switch (tokenType) {
+ case 'string':
+ return { color: isDarkMode ? '#9cdcfe' : 'green' }
+ case 'number':
+ return { color: isDarkMode ? '#b5cea8' : 'darkorange' }
+ case 'boolean':
+ return { color: isDarkMode ? '#569cd6' : 'blue' }
+ case 'null':
+ return { color: isDarkMode ? '#d4d4d4' : 'magenta' }
+ case 'key':
+ return { color: isDarkMode ? '#ff5733' : '#ff5733' }
+ default:
+ return {}
}
- )
+ }
+
+ return {children}
+}
+
+function parseJsonToElements(json, isDarkMode) {
+ if (!json) return []
+
+ const tokens = []
+ let index = 0
+
+ // Escape HTML characters for safety
+ const escapedJson = json.replace(/&/g, '&').replace(//g, '>')
+
+ // eslint-disable-next-line
+ const tokenRegex = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g
+
+ let match
+ let lastIndex = 0
+
+ while ((match = tokenRegex.exec(escapedJson)) !== null) {
+ // Add any text before the match as plain text
+ if (match.index > lastIndex) {
+ const plainText = escapedJson.substring(lastIndex, match.index)
+ if (plainText) {
+ tokens.push({plainText})
+ }
+ }
+
+ // Determine token type
+ let tokenType = 'number'
+ const matchText = match[0]
+
+ if (/^"/.test(matchText)) {
+ if (/:$/.test(matchText)) {
+ tokenType = 'key'
+ } else {
+ tokenType = 'string'
+ }
+ } else if (/true|false/.test(matchText)) {
+ tokenType = 'boolean'
+ } else if (/null/.test(matchText)) {
+ tokenType = 'null'
+ }
+
+ tokens.push(
+
+ {matchText}
+
+ )
+
+ lastIndex = match.index + match[0].length
+ }
+
+ // Add any remaining text
+ if (lastIndex < escapedJson.length) {
+ const remainingText = escapedJson.substring(lastIndex)
+ if (remainingText) {
+ tokens.push({remainingText})
+ }
+ }
+
+ return tokens
}
export const JSONViewer = ({ data, maxHeight = '400px' }) => {
@@ -35,6 +89,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => {
const customization = useSelector((state) => state.customization)
const isDarkMode = customization.isDarkMode
+ const jsonString = JSON.stringify(data, null, 2)
+ const jsonElements = parseJsonToElements(jsonString, isDarkMode)
+
return (
{
maxHeight: maxHeight
}}
>
-
{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
- dangerouslySetInnerHTML={{
- __html: syntaxHighlight(JSON.stringify(data, null, 2), isDarkMode)
- }}
- />
+ >
+ {jsonElements}
+
)
}
@@ -85,3 +124,9 @@ JSONViewer.propTypes = {
data: PropTypes.object,
maxHeight: PropTypes.string
}
+
+JsonToken.propTypes = {
+ type: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ isDarkMode: PropTypes.bool.isRequired
+}
diff --git a/packages/ui/src/ui-component/safe/SafeHTML.jsx b/packages/ui/src/ui-component/safe/SafeHTML.jsx
new file mode 100644
index 000000000..3ef11b3fe
--- /dev/null
+++ b/packages/ui/src/ui-component/safe/SafeHTML.jsx
@@ -0,0 +1,58 @@
+import PropTypes from 'prop-types'
+import DOMPurify from 'dompurify'
+
+/**
+ * SafeHTML component that sanitizes HTML content before rendering
+ */
+export const SafeHTML = ({ html, allowedTags, allowedAttributes, ...props }) => {
+ // Configure DOMPurify options
+ const config = {
+ ALLOWED_TAGS: allowedTags || [
+ 'p',
+ 'br',
+ 'strong',
+ 'em',
+ 'u',
+ 'i',
+ 'b',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'ul',
+ 'ol',
+ 'li',
+ 'blockquote',
+ 'pre',
+ 'code',
+ 'a',
+ 'img',
+ 'table',
+ 'thead',
+ 'tbody',
+ 'tr',
+ 'th',
+ 'td',
+ 'div',
+ 'span'
+ ],
+ ALLOWED_ATTR: allowedAttributes || ['href', 'title', 'alt', 'src', 'class', 'id', 'style'],
+ ALLOW_DATA_ATTR: false,
+ FORBID_SCRIPT: true,
+ FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input'],
+ FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
+ }
+
+ // Sanitize the HTML content
+ const sanitizedHTML = DOMPurify.sanitize(html || '', config)
+
+ return
+}
+
+SafeHTML.propTypes = {
+ html: PropTypes.string.isRequired,
+ allowedTags: PropTypes.arrayOf(PropTypes.string),
+ allowedAttributes: PropTypes.arrayOf(PropTypes.string)
+}
diff --git a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx
index 5a5296912..5be0f4c93 100644
--- a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx
+++ b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx
@@ -29,6 +29,7 @@ import toolSVG from '@/assets/images/tool.svg'
// Project imports
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
+import { SafeHTML } from '@/ui-component/safe/SafeHTML'
import { AGENTFLOW_ICONS, baseURL } from '@/store/constant'
import { JSONViewer } from '@/ui-component/json/JsonViewer'
import ReactJson from 'flowise-react-json-view'
@@ -708,7 +709,7 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic,
backgroundColor: theme.palette.background.paper
}}
>
-
+
)
} else {
diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx
index 34a5e402b..b3eb5ed27 100644
--- a/packages/ui/src/views/chatmessage/ChatMessage.jsx
+++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx
@@ -49,6 +49,7 @@ import audioUploadSVG from '@/assets/images/wave-sound.jpg'
// project import
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
+import { SafeHTML } from '@/ui-component/safe/SafeHTML'
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
@@ -1659,7 +1660,7 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP
} else if (item.type === 'html') {
return (
)
} else {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fafa45f26..865ce3dc1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -984,6 +984,9 @@ importers:
clsx:
specifier: ^1.1.1
version: 1.2.1
+ dompurify:
+ specifier: ^3.2.6
+ version: 3.2.6
dotenv:
specifier: ^16.0.0
version: 16.4.5
@@ -9941,6 +9944,9 @@ packages:
resolution: { integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== }
engines: { node: '>= 4' }
+ dompurify@3.2.6:
+ resolution: { integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== }
+
domutils@1.7.0:
resolution: { integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== }
@@ -30197,6 +30203,10 @@ snapshots:
dependencies:
domelementtype: 2.3.0
+ dompurify@3.2.6:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
domutils@1.7.0:
dependencies:
dom-serializer: 0.2.2