From cf6539cd3f9a5c08c7cf1ea628f74f171c9c1c1a Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 18 Sep 2025 19:18:50 +0100 Subject: [PATCH] Bugfix/Parse JSON correctly (#5220) * parse JSON correctly * add codeblock highlight --- .../agentflow/ExecuteFlow/ExecuteFlow.ts | 5 +- .../components/nodes/agentflow/HTTP/HTTP.ts | 17 +---- .../nodes/agentflow/Iteration/Iteration.ts | 6 +- .../nodes/tools/MCP/CustomMCP/CustomMCP.ts | 5 +- .../tools/RequestsDelete/RequestsDelete.ts | 5 +- .../nodes/tools/RequestsDelete/core.ts | 6 +- .../nodes/tools/RequestsGet/RequestsGet.ts | 5 +- .../nodes/tools/RequestsGet/core.ts | 6 +- .../nodes/tools/RequestsPost/RequestsPost.ts | 7 +- .../nodes/tools/RequestsPost/core.ts | 4 +- .../nodes/tools/RequestsPut/RequestsPut.ts | 7 +- .../nodes/tools/RequestsPut/core.ts | 4 +- packages/components/src/utils.ts | 70 +++++++++++++++++- packages/ui/package.json | 2 + packages/ui/src/assets/scss/style.scss | 74 +++++++++++++++++++ .../dialog/ExpandRichInputDialog.jsx | 58 ++++++++++----- .../ui/src/ui-component/input/RichInput.jsx | 33 ++++++++- pnpm-lock.yaml | 38 ++++++++++ 18 files changed, 283 insertions(+), 69 deletions(-) diff --git a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts index 06bc6746..e2f0765a 100644 --- a/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts +++ b/packages/components/nodes/agentflow/ExecuteFlow/ExecuteFlow.ts @@ -8,8 +8,7 @@ import { IServerSideEventStreamer } from '../../../src/Interface' import axios, { AxiosRequestConfig } from 'axios' -import { getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils' -import JSON5 from 'json5' +import { getCredentialData, getCredentialParam, processTemplateVariables, parseJsonBody } from '../../../src/utils' import { DataSource } from 'typeorm' import { BaseMessageLike } from '@langchain/core/messages' import { updateFlowState } from '../utils' @@ -168,7 +167,7 @@ class ExecuteFlow_Agentflow implements INode { let overrideConfig = nodeData.inputs?.executeFlowOverrideConfig if (typeof overrideConfig === 'string' && overrideConfig.startsWith('{') && overrideConfig.endsWith('}')) { try { - overrideConfig = JSON5.parse(overrideConfig) + overrideConfig = parseJsonBody(overrideConfig) } catch (parseError) { throw new Error(`Invalid JSON in executeFlowOverrideConfig: ${parseError.message}`) } diff --git a/packages/components/nodes/agentflow/HTTP/HTTP.ts b/packages/components/nodes/agentflow/HTTP/HTTP.ts index 405881a4..77f1f431 100644 --- a/packages/components/nodes/agentflow/HTTP/HTTP.ts +++ b/packages/components/nodes/agentflow/HTTP/HTTP.ts @@ -2,9 +2,8 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter import { AxiosRequestConfig, Method, ResponseType } from 'axios' import FormData from 'form-data' import * as querystring from 'querystring' -import { getCredentialData, getCredentialParam } from '../../../src/utils' +import { getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils' import { secureAxiosRequest } from '../../../src/httpSecurity' -import JSON5 from 'json5' class HTTP_Agentflow implements INode { label: string @@ -20,16 +19,6 @@ class HTTP_Agentflow implements INode { credential: INodeParams inputs: INodeParams[] - private parseJsonBody(body: string): any { - try { - return JSON5.parse(body) - } catch (error) { - throw new Error( - `Invalid JSON format in body. Original error: ${error.message}. Please ensure your JSON is properly formatted with quoted strings and valid escape sequences.` - ) - } - } - constructor() { this.label = 'HTTP' this.name = 'httpAgentflow' @@ -285,7 +274,7 @@ class HTTP_Agentflow implements INode { if (method !== 'GET' && body) { switch (bodyType) { case 'json': { - requestConfig.data = typeof body === 'string' ? this.parseJsonBody(body) : body + requestConfig.data = typeof body === 'string' ? parseJsonBody(body) : body requestHeaders['Content-Type'] = 'application/json' break } @@ -303,7 +292,7 @@ class HTTP_Agentflow implements INode { break } case 'xWwwFormUrlencoded': - requestConfig.data = querystring.stringify(typeof body === 'string' ? this.parseJsonBody(body) : body) + requestConfig.data = querystring.stringify(typeof body === 'string' ? parseJsonBody(body) : body) requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded' break } diff --git a/packages/components/nodes/agentflow/Iteration/Iteration.ts b/packages/components/nodes/agentflow/Iteration/Iteration.ts index 04dfbb51..145602b9 100644 --- a/packages/components/nodes/agentflow/Iteration/Iteration.ts +++ b/packages/components/nodes/agentflow/Iteration/Iteration.ts @@ -1,5 +1,5 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import JSON5 from 'json5' +import { parseJsonBody } from '../../../src/utils' class Iteration_Agentflow implements INode { label: string @@ -42,10 +42,10 @@ class Iteration_Agentflow implements INode { // Helper function to clean JSON strings with redundant backslashes const safeParseJson = (str: string): string => { try { - return JSON5.parse(str) + return parseJsonBody(str) } catch { // Try parsing after cleaning - return JSON5.parse(str.replace(/\\(["'[\]{}])/g, '$1')) + return parseJsonBody(str.replace(/\\(["'[\]{}])/g, '$1')) } } diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index 1e03f31f..09015a71 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -1,10 +1,9 @@ import { Tool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' import { MCPToolkit, validateMCPServerConfig } from '../core' -import { getVars, prepareSandboxVars } from '../../../../src/utils' +import { getVars, prepareSandboxVars, parseJsonBody } from '../../../../src/utils' import { DataSource } from 'typeorm' import hash from 'object-hash' -import JSON5 from 'json5' const mcpServerConfig = `{ "command": "npx", @@ -270,7 +269,7 @@ function substituteVariablesInString(str: string, sandbox: any): string { function convertToValidJSONString(inputString: string) { try { - const jsObject = JSON5.parse(inputString) + const jsObject = parseJsonBody(inputString) return JSON.stringify(jsObject, null, 2) } catch (error) { console.error('Error converting to JSON:', error) diff --git a/packages/components/nodes/tools/RequestsDelete/RequestsDelete.ts b/packages/components/nodes/tools/RequestsDelete/RequestsDelete.ts index e66d40b5..22a5bb8e 100644 --- a/packages/components/nodes/tools/RequestsDelete/RequestsDelete.ts +++ b/packages/components/nodes/tools/RequestsDelete/RequestsDelete.ts @@ -1,7 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils' +import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils' import { desc, RequestParameters, RequestsDeleteTool } from './core' -import JSON5 from 'json5' const codeExample = `{ "id": { @@ -131,7 +130,7 @@ class RequestsDelete_Tools implements INode { if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10) if (headers) { - const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers)) + const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers)) obj.headers = parsedHeaders } diff --git a/packages/components/nodes/tools/RequestsDelete/core.ts b/packages/components/nodes/tools/RequestsDelete/core.ts index 1c71911d..47e8b586 100644 --- a/packages/components/nodes/tools/RequestsDelete/core.ts +++ b/packages/components/nodes/tools/RequestsDelete/core.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' import { secureFetch } from '../../../src/httpSecurity' -import JSON5 from 'json5' +import { parseJsonBody } from '../../../src/utils' export const desc = `Use this when you need to execute a DELETE request to remove data from a website.` @@ -23,7 +23,7 @@ const createRequestsDeleteSchema = (queryParamsSchema?: string) => { // If queryParamsSchema is provided, parse it and add dynamic query params if (queryParamsSchema) { try { - const parsedSchema = JSON5.parse(queryParamsSchema) + const parsedSchema = parseJsonBody(queryParamsSchema) const queryParamsObject: Record = {} Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => { @@ -109,7 +109,7 @@ export class RequestsDeleteTool extends DynamicStructuredTool { if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) { try { - const parsedSchema = JSON5.parse(this.queryParamsSchema) + const parsedSchema = parseJsonBody(this.queryParamsSchema) const pathParams: Array<{ key: string; value: string }> = [] Object.entries(params.queryParams).forEach(([key, value]) => { diff --git a/packages/components/nodes/tools/RequestsGet/RequestsGet.ts b/packages/components/nodes/tools/RequestsGet/RequestsGet.ts index 258c3807..6a7eed98 100644 --- a/packages/components/nodes/tools/RequestsGet/RequestsGet.ts +++ b/packages/components/nodes/tools/RequestsGet/RequestsGet.ts @@ -1,7 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils' +import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils' import { desc, RequestParameters, RequestsGetTool } from './core' -import JSON5 from 'json5' const codeExample = `{ "id": { @@ -131,7 +130,7 @@ class RequestsGet_Tools implements INode { if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10) if (headers) { - const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers)) + const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers)) obj.headers = parsedHeaders } diff --git a/packages/components/nodes/tools/RequestsGet/core.ts b/packages/components/nodes/tools/RequestsGet/core.ts index b519341a..ca2b07d4 100644 --- a/packages/components/nodes/tools/RequestsGet/core.ts +++ b/packages/components/nodes/tools/RequestsGet/core.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' import { secureFetch } from '../../../src/httpSecurity' -import JSON5 from 'json5' +import { parseJsonBody } from '../../../src/utils' export const desc = `Use this when you need to execute a GET request to get data from a website.` @@ -23,7 +23,7 @@ const createRequestsGetSchema = (queryParamsSchema?: string) => { // If queryParamsSchema is provided, parse it and add dynamic query params if (queryParamsSchema) { try { - const parsedSchema = JSON5.parse(queryParamsSchema) + const parsedSchema = parseJsonBody(queryParamsSchema) const queryParamsObject: Record = {} Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => { @@ -109,7 +109,7 @@ export class RequestsGetTool extends DynamicStructuredTool { if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) { try { - const parsedSchema = JSON5.parse(this.queryParamsSchema) + const parsedSchema = parseJsonBody(this.queryParamsSchema) const pathParams: Array<{ key: string; value: string }> = [] Object.entries(params.queryParams).forEach(([key, value]) => { diff --git a/packages/components/nodes/tools/RequestsPost/RequestsPost.ts b/packages/components/nodes/tools/RequestsPost/RequestsPost.ts index ea9c8669..3341f05d 100644 --- a/packages/components/nodes/tools/RequestsPost/RequestsPost.ts +++ b/packages/components/nodes/tools/RequestsPost/RequestsPost.ts @@ -1,7 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils' +import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils' import { RequestParameters, desc, RequestsPostTool } from './core' -import JSON5 from 'json5' const codeExample = `{ "name": { @@ -141,11 +140,11 @@ class RequestsPost_Tools implements INode { if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema) if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10) if (headers) { - const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers)) + const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers)) obj.headers = parsedHeaders } if (body) { - const parsedBody = typeof body === 'object' ? body : JSON5.parse(body) + const parsedBody = typeof body === 'object' ? body : parseJsonBody(body) obj.body = parsedBody } diff --git a/packages/components/nodes/tools/RequestsPost/core.ts b/packages/components/nodes/tools/RequestsPost/core.ts index 5ee082c4..96eb7981 100644 --- a/packages/components/nodes/tools/RequestsPost/core.ts +++ b/packages/components/nodes/tools/RequestsPost/core.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' import { secureFetch } from '../../../src/httpSecurity' -import JSON5 from 'json5' +import { parseJsonBody } from '../../../src/utils' export const desc = `Use this when you want to execute a POST request to create or update a resource.` @@ -28,7 +28,7 @@ const createRequestsPostSchema = (bodySchema?: string) => { // If bodySchema is provided, parse it and add dynamic body params if (bodySchema) { try { - const parsedSchema = JSON5.parse(bodySchema) + const parsedSchema = parseJsonBody(bodySchema) const bodyParamsObject: Record = {} Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => { diff --git a/packages/components/nodes/tools/RequestsPut/RequestsPut.ts b/packages/components/nodes/tools/RequestsPut/RequestsPut.ts index ce5bfe38..3e0bab4e 100644 --- a/packages/components/nodes/tools/RequestsPut/RequestsPut.ts +++ b/packages/components/nodes/tools/RequestsPut/RequestsPut.ts @@ -1,7 +1,6 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils' +import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils' import { RequestParameters, desc, RequestsPutTool } from './core' -import JSON5 from 'json5' const codeExample = `{ "name": { @@ -141,11 +140,11 @@ class RequestsPut_Tools implements INode { if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema) if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10) if (headers) { - const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers)) + const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers)) obj.headers = parsedHeaders } if (body) { - const parsedBody = typeof body === 'object' ? body : JSON5.parse(body) + const parsedBody = typeof body === 'object' ? body : parseJsonBody(body) obj.body = parsedBody } diff --git a/packages/components/nodes/tools/RequestsPut/core.ts b/packages/components/nodes/tools/RequestsPut/core.ts index 0984be33..2db2b886 100644 --- a/packages/components/nodes/tools/RequestsPut/core.ts +++ b/packages/components/nodes/tools/RequestsPut/core.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { DynamicStructuredTool } from '../OpenAPIToolkit/core' import { secureFetch } from '../../../src/httpSecurity' -import JSON5 from 'json5' +import { parseJsonBody } from '../../../src/utils' export const desc = `Use this when you want to execute a PUT request to update or replace a resource.` @@ -28,7 +28,7 @@ const createRequestsPutSchema = (bodySchema?: string) => { // If bodySchema is provided, parse it and add dynamic body params if (bodySchema) { try { - const parsedSchema = JSON5.parse(bodySchema) + const parsedSchema = parseJsonBody(bodySchema) const bodyParamsObject: Record = {} Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => { diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index d07bbd06..05e4e418 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1408,7 +1408,7 @@ const parseOutput = (output: any): any => { // Check if it looks like JSON (starts with { or [) if ((trimmedOutput.startsWith('{') && trimmedOutput.endsWith('}')) || (trimmedOutput.startsWith('[') && trimmedOutput.endsWith(']'))) { try { - const parsedOutput = JSON5.parse(trimmedOutput) + const parsedOutput = parseJsonBody(trimmedOutput) return parsedOutput } catch (e) { return output @@ -1659,3 +1659,71 @@ export const processTemplateVariables = (state: ICommonObject, finalOutput: any) return newState } + +/** + * Parse JSON body with comprehensive error handling and cleanup + * @param {string} body - The JSON string to parse + * @returns {any} - The parsed JSON object + * @throws {Error} - Detailed error message with suggestions for common JSON issues + */ +export const parseJsonBody = (body: string): any => { + try { + // First try to parse as-is with JSON5 (which handles more cases than standard JSON) + return JSON5.parse(body) + } catch (error) { + try { + // If that fails, try to clean up common issues + let cleanedBody = body + + // 1. Remove unnecessary backslash escapes for square brackets and braces + // eslint-disable-next-line + cleanedBody = cleanedBody.replace(/\\(?=[\[\]{}])/g, '') + + // 2. Fix single quotes to double quotes (but preserve quotes inside strings) + cleanedBody = cleanedBody.replace(/'/g, '"') + + // 3. Remove trailing commas before closing brackets/braces + cleanedBody = cleanedBody.replace(/,(\s*[}\]])/g, '$1') + + // 4. Remove comments (// and /* */) + cleanedBody = cleanedBody + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + + return JSON5.parse(cleanedBody) + } catch (secondError) { + try { + // 3rd attempt: try with standard JSON.parse on original body + return JSON.parse(body) + } catch (thirdError) { + try { + // 4th attempt: try with standard JSON.parse on cleaned body + const finalCleanedBody = body + // eslint-disable-next-line + .replace(/\\(?=[\[\]{}])/g, '') // Basic escape cleanup + .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas + .trim() + + return JSON.parse(finalCleanedBody) + } catch (fourthError) { + // Provide comprehensive error message with suggestions + const suggestions = [ + '• Ensure all strings are enclosed in double quotes', + '• Remove trailing commas', + '• Remove comments (// or /* */)', + '• Escape special characters properly (\\n for newlines, \\" for quotes)', + '• Use double quotes instead of single quotes', + '• Remove unnecessary backslashes before brackets [ ] { }' + ] + + throw new Error( + `Invalid JSON format in body. Original error: ${error.message}. ` + + `After cleanup attempts: ${secondError.message}. 3rd attempt: ${thirdError.message}. Final attempt: ${fourthError.message}.\n\n` + + `Common fixes:\n${suggestions.join('\n')}\n\n` + + `Received body: ${body.substring(0, 200)}${body.length > 200 ? '...' : ''}` + ) + } + } + } + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index c93b7794..cf44633a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,6 +26,7 @@ "@mui/x-tree-view": "^7.25.0", "@reduxjs/toolkit": "^2.2.7", "@tabler/icons-react": "^3.30.0", + "@tiptap/extension-code-block-lowlight": "^3.4.3", "@tiptap/extension-mention": "^2.11.5", "@tiptap/extension-placeholder": "^2.11.5", "@tiptap/pm": "^2.11.5", @@ -46,6 +47,7 @@ "history": "^5.0.0", "html-react-parser": "^3.0.4", "lodash": "^4.17.21", + "lowlight": "^3.3.0", "moment": "^2.29.3", "notistack": "^2.0.4", "prop-types": "^15.7.2", diff --git a/packages/ui/src/assets/scss/style.scss b/packages/ui/src/assets/scss/style.scss index 52e70040..bda0dbb7 100644 --- a/packages/ui/src/assets/scss/style.scss +++ b/packages/ui/src/assets/scss/style.scss @@ -132,6 +132,80 @@ content: '\200B'; } } + + pre { + background: var(--code-bg, #2d2d2d) !important; + border-radius: 0.5rem; + color: var(--code-color, #d4d4d4) !important; + font-family: 'JetBrainsMono', 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + code { + background: none !important; + color: inherit !important; + font-size: 0.8rem; + padding: 0; + } + + /* Syntax highlighting matching the screenshot colors */ + .hljs-comment, + .hljs-quote { + color: var(--hljs-comment, #6a9955) !important; + } + + .hljs-variable, + .hljs-name { + color: var(--hljs-variable, #9cdcfe) !important; /* Light blue for variables */ + } + + .hljs-number, + .hljs-literal { + color: var(--hljs-number, #b5cea8) !important; /* Light green for numbers */ + } + + .hljs-string { + color: var(--hljs-string, #ce9178) !important; /* Orange/peach for strings */ + } + + .hljs-title, + .hljs-built_in, + .hljs-builtin-name { + color: var(--hljs-title, #dcdcaa) !important; /* Yellow for function names */ + } + + .hljs-keyword, + .hljs-selector-tag { + color: var(--hljs-keyword, #569cd6) !important; /* Blue for keywords */ + } + + /* Additional elements that should match the base text color */ + .hljs-operator, + .hljs-punctuation, + .hljs-template-variable, + .hljs-attribute, + .hljs-tag, + .hljs-regexp, + .hljs-link, + .hljs-selector-id, + .hljs-selector-class, + .hljs-meta, + .hljs-type, + .hljs-params, + .hljs-symbol, + .hljs-bullet, + .hljs-section { + color: var(--code-color, #d4d4d4) !important; /* Default text color */ + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; + } + } } .spin-animation { diff --git a/packages/ui/src/ui-component/dialog/ExpandRichInputDialog.jsx b/packages/ui/src/ui-component/dialog/ExpandRichInputDialog.jsx index 4fb5204e..ae94e97d 100644 --- a/packages/ui/src/ui-component/dialog/ExpandRichInputDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ExpandRichInputDialog.jsx @@ -1,6 +1,6 @@ import { createPortal } from 'react-dom' import { useState, useEffect } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import PropTypes from 'prop-types' import PerfectScrollbar from 'react-perfect-scrollbar' @@ -17,14 +17,18 @@ import Placeholder from '@tiptap/extension-placeholder' import { mergeAttributes } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import Mention from '@tiptap/extension-mention' +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import { common, createLowlight } from 'lowlight' import { suggestionOptions } from '@/ui-component/input/suggestionOption' import { getAvailableNodesForVariable } from '@/utils/genericHelper' +const lowlight = createLowlight(common) + // Store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' // Add styled component for editor wrapper -const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({ +const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDarkMode }) => ({ '& .ProseMirror': { padding: '0px 14px', height: rows ? `${rows * 1.4375}rem` : '2.4rem', @@ -32,40 +36,48 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({ overflowX: rows ? 'auto' : 'hidden', lineHeight: rows ? '1.4375em' : '0.875em', fontWeight: 500, - color: theme.palette.grey[900], - border: `1px solid ${theme.palette.textBackground.border}`, + color: disabled ? theme.palette.action.disabled : theme.palette.grey[900], + border: `1px solid ${theme.palette.grey[900] + 25}`, borderRadius: '10px', backgroundColor: theme.palette.textBackground.main, boxSizing: 'border-box', whiteSpace: rows ? 'pre-wrap' : 'nowrap', '&:hover': { - borderColor: theme.palette.text.primary, - cursor: 'text' + borderColor: disabled ? `${theme.palette.grey[900] + 25}` : theme.palette.text.primary, + cursor: disabled ? 'default' : 'text' }, '&:focus': { - borderColor: theme.palette.primary.main, - boxShadow: `0 0 0 0px ${theme.palette.primary.main}`, + borderColor: disabled ? `${theme.palette.grey[900] + 25}` : theme.palette.primary.main, outline: 'none' }, - '&[disabled]': { - backgroundColor: theme.palette.action.disabledBackground, - color: theme.palette.action.disabled - }, // Placeholder for first paragraph when editor is empty '& p.is-editor-empty:first-of-type::before': { content: 'attr(data-placeholder)', float: 'left', - color: theme.palette.text.primary, - opacity: 0.4, + color: disabled ? theme.palette.action.disabled : theme.palette.text.primary, + opacity: disabled ? 0.6 : 0.4, pointerEvents: 'none', height: 0 - } + }, + // Set CSS custom properties for theme-aware styling based on the screenshot + '--code-bg': isDarkMode ? '#2d2d2d' : '#f5f5f5', + '--code-color': isDarkMode ? '#d4d4d4' : '#333333', + '--hljs-comment': isDarkMode ? '#6a9955' : '#6a9955', + '--hljs-variable': isDarkMode ? '#9cdcfe' : '#d73a49', // Light blue for variables (var, i) + '--hljs-number': isDarkMode ? '#b5cea8' : '#e36209', // Light green for numbers (1, 20, 15, etc.) + '--hljs-string': isDarkMode ? '#ce9178' : '#22863a', // Orange/peach for strings ("FizzBuzz", "Fizz", "Buzz") + '--hljs-title': isDarkMode ? '#dcdcaa' : '#6f42c1', // Yellow for function names (log) + '--hljs-keyword': isDarkMode ? '#569cd6' : '#005cc5', // Blue for keywords (for, if, else) + '--hljs-operator': isDarkMode ? '#d4d4d4' : '#333333', // White/gray for operators (=, %, ==, etc.) + '--hljs-punctuation': isDarkMode ? '#d4d4d4' : '#333333' // White/gray for punctuation ({, }, ;, etc.) } })) // define your extension array const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [ - StarterKit, + StarterKit.configure({ + codeBlock: false + }), Mention.configure({ HTMLAttributes: { class: 'variable' @@ -86,6 +98,11 @@ const extensions = (availableNodesForVariable, availableState, acceptNodeOutputA isNodeInsideInteration ), deleteTriggerWithBackspace: true + }), + CodeBlockLowlight.configure({ + lowlight, + enableTabIndentation: true, + tabSize: 2 }) ] @@ -93,6 +110,8 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC const portalElement = document.getElementById('portal') const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) + const isDarkMode = customization.isDarkMode const [inputValue, setInputValue] = useState('') const [inputParam, setInputParam] = useState(null) @@ -201,7 +220,12 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC }} > - + diff --git a/packages/ui/src/ui-component/input/RichInput.jsx b/packages/ui/src/ui-component/input/RichInput.jsx index 7be62551..6ae4be38 100644 --- a/packages/ui/src/ui-component/input/RichInput.jsx +++ b/packages/ui/src/ui-component/input/RichInput.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' import { useEditor, EditorContent } from '@tiptap/react' import Placeholder from '@tiptap/extension-placeholder' import { mergeAttributes } from '@tiptap/core' @@ -7,12 +8,18 @@ import StarterKit from '@tiptap/starter-kit' import { styled } from '@mui/material/styles' import { Box } from '@mui/material' import Mention from '@tiptap/extension-mention' +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import { common, createLowlight } from 'lowlight' import { suggestionOptions } from './suggestionOption' import { getAvailableNodesForVariable } from '@/utils/genericHelper' +const lowlight = createLowlight(common) + // define your extension array const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [ - StarterKit, + StarterKit.configure({ + codeBlock: false + }), Mention.configure({ HTMLAttributes: { class: 'variable' @@ -33,11 +40,16 @@ const extensions = (availableNodesForVariable, availableState, acceptNodeOutputA isNodeInsideInteration ), deleteTriggerWithBackspace: true + }), + CodeBlockLowlight.configure({ + lowlight, + enableTabIndentation: true, + tabSize: 2 }) ] // Add styled component for editor wrapper -const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled }) => ({ +const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDarkMode }) => ({ '& .ProseMirror': { padding: '0px 14px', height: rows ? `${rows * 1.4375}rem` : '2.4rem', @@ -67,11 +79,24 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled }) => opacity: disabled ? 0.6 : 0.4, pointerEvents: 'none', height: 0 - } + }, + // Set CSS custom properties for theme-aware styling based on the screenshot + '--code-bg': isDarkMode ? '#2d2d2d' : '#f5f5f5', + '--code-color': isDarkMode ? '#d4d4d4' : '#333333', + '--hljs-comment': isDarkMode ? '#6a9955' : '#6a9955', + '--hljs-variable': isDarkMode ? '#9cdcfe' : '#d73a49', // Light blue for variables (var, i) + '--hljs-number': isDarkMode ? '#b5cea8' : '#e36209', // Light green for numbers (1, 20, 15, etc.) + '--hljs-string': isDarkMode ? '#ce9178' : '#22863a', // Orange/peach for strings ("FizzBuzz", "Fizz", "Buzz") + '--hljs-title': isDarkMode ? '#dcdcaa' : '#6f42c1', // Yellow for function names (log) + '--hljs-keyword': isDarkMode ? '#569cd6' : '#005cc5', // Blue for keywords (for, if, else) + '--hljs-operator': isDarkMode ? '#d4d4d4' : '#333333', // White/gray for operators (=, %, ==, etc.) + '--hljs-punctuation': isDarkMode ? '#d4d4d4' : '#333333' // White/gray for punctuation ({, }, ;, etc.) } })) export const RichInput = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => { + const customization = useSelector((state) => state.customization) + const isDarkMode = customization.isDarkMode const [availableNodesForVariable, setAvailableNodesForVariable] = useState([]) const [availableState, setAvailableState] = useState([]) const [nodeData, setNodeData] = useState({}) @@ -117,7 +142,7 @@ export const RichInput = ({ inputParam, value, nodes, edges, nodeId, onChange, d return ( - + ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61da213b..f843fb62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -977,6 +977,9 @@ importers: '@tabler/icons-react': specifier: ^3.30.0 version: 3.31.0(react@18.2.0) + '@tiptap/extension-code-block-lowlight': + specifier: ^3.4.3 + version: 3.4.3(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-code-block@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(highlight.js@11.11.1)(lowlight@3.3.0) '@tiptap/extension-mention': specifier: ^2.11.5 version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(@tiptap/suggestion@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)) @@ -1037,6 +1040,9 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + lowlight: + specifier: ^3.3.0 + version: 3.3.0 moment: specifier: ^2.29.3 version: 2.30.1 @@ -7135,6 +7141,15 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-code-block-lowlight@3.4.3': + resolution: { integrity: sha512-d3vEhv2cLDdWL2unqRkF0LkGEDEm/pJCWZwSjqzYFe3vrTEsTExorpwZoVV8i9uX/DPG4AdM+mf3hLK9L5FZoA== } + peerDependencies: + '@tiptap/core': ^3.4.3 + '@tiptap/extension-code-block': ^3.4.3 + '@tiptap/pm': ^3.4.3 + highlight.js: ^11 + lowlight: ^2 || ^3 + '@tiptap/extension-code-block@2.12.0': resolution: { integrity: sha512-1D7cYAjgxEFHdfC/35Ooi4GqWKB5sszbW8iI7N16XILNln26xb0d5KflXqYrwr9CN/ZnZoCl2o6YsP7xEObcZA== } peerDependencies: @@ -11595,6 +11610,10 @@ packages: highlight.js@10.7.3: resolution: { integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== } + highlight.js@11.11.1: + resolution: { integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w== } + engines: { node: '>=12.0.0' } + history@5.3.0: resolution: { integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== } @@ -13090,6 +13109,9 @@ packages: lowlight@1.20.0: resolution: { integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== } + lowlight@3.3.0: + resolution: { integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ== } + lru-cache@10.2.0: resolution: { integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== } engines: { node: 14 || >=16.14 } @@ -27410,6 +27432,14 @@ snapshots: dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) + '@tiptap/extension-code-block-lowlight@3.4.3(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/extension-code-block@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) + '@tiptap/extension-code-block': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0) + '@tiptap/pm': 2.12.0 + highlight.js: 11.11.1 + lowlight: 3.3.0 + '@tiptap/extension-code-block@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)': dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) @@ -33109,6 +33139,8 @@ snapshots: highlight.js@10.7.3: {} + highlight.js@11.11.1: {} + history@5.3.0: dependencies: '@babel/runtime': 7.24.0 @@ -35299,6 +35331,12 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + lru-cache@10.2.0: {} lru-cache@4.1.5: