Fix: Introducing a new JSON editor (#11401)

### What problem does this PR solve?

Fix: Introducing a new JSON editor

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-11-20 12:44:32 +08:00
committed by GitHub
parent fa5cf10f56
commit ea0352ee4a
7 changed files with 799 additions and 36 deletions

View File

@ -79,6 +79,7 @@
"input-otp": "^1.4.1",
"js-base64": "^3.7.5",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.4.2",
"lexical": "^0.23.1",
"lodash": "^4.17.21",
"lucide-react": "^0.546.0",

View File

@ -0,0 +1,132 @@
.ace-tomorrow-night .ace_gutter {
background: var(--bg-card);
color: rgb(var(--text-primary));
}
.ace-tomorrow-night .ace_print-margin {
width: 1px;
background: #25282c;
}
.ace-tomorrow-night {
background: var(--bg-card);
color: rgb(var(--text-primary));
.ace_editor {
background: var(--bg-card);
}
}
.ace-tomorrow-night .ace_cursor {
color: #aeafad;
}
.ace-tomorrow-night .ace_marker-layer .ace_selection {
background: #373b41;
}
.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {
box-shadow: 0 0 3px 0px #1d1f21;
}
.ace-tomorrow-night .ace_marker-layer .ace_step {
background: rgb(102, 82, 0);
}
.ace-tomorrow-night .ace_marker-layer .ace_bracket {
margin: -1px 0 0 -1px;
border: 1px solid #4b4e55;
}
.ace-tomorrow-night .ace_marker-layer .ace_active-line {
background: var(--bg-card);
}
.ace-tomorrow-night .ace_gutter-active-line {
background-color: var(--bg-card);
}
.ace-tomorrow-night .ace_marker-layer .ace_selected-word {
border: 1px solid #373b41;
}
.ace-tomorrow-night .ace_invisible {
color: #4b4e55;
}
.ace-tomorrow-night .ace_keyword,
.ace-tomorrow-night .ace_meta,
.ace-tomorrow-night .ace_storage,
.ace-tomorrow-night .ace_storage.ace_type,
.ace-tomorrow-night .ace_support.ace_type {
color: #b294bb;
}
.ace-tomorrow-night .ace_keyword.ace_operator {
color: #8abeb7;
}
.ace-tomorrow-night .ace_constant.ace_character,
.ace-tomorrow-night .ace_constant.ace_language,
.ace-tomorrow-night .ace_constant.ace_numeric,
.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,
.ace-tomorrow-night .ace_support.ace_constant,
.ace-tomorrow-night .ace_variable.ace_parameter {
color: #de935f;
}
.ace-tomorrow-night .ace_constant.ace_other {
color: #ced1cf;
}
.ace-tomorrow-night .ace_invalid {
color: #ced2cf;
background-color: #df5f5f;
}
.ace-tomorrow-night .ace_invalid.ace_deprecated {
color: #ced2cf;
background-color: #b798bf;
}
.ace-tomorrow-night .ace_fold {
background-color: #81a2be;
border-color: #c5c8c6;
}
.ace-tomorrow-night .ace_entity.ace_name.ace_function,
.ace-tomorrow-night .ace_support.ace_function,
.ace-tomorrow-night .ace_variable {
color: #81a2be;
}
.ace-tomorrow-night .ace_support.ace_class,
.ace-tomorrow-night .ace_support.ace_type {
color: #f0c674;
}
.ace-tomorrow-night .ace_heading,
.ace-tomorrow-night .ace_markup.ace_heading,
.ace-tomorrow-night .ace_string {
color: #b5bd68;
}
.ace-tomorrow-night .ace_entity.ace_name.ace_tag,
.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,
.ace-tomorrow-night .ace_meta.ace_tag,
.ace-tomorrow-night .ace_string.ace_regexp,
.ace-tomorrow-night .ace_variable {
color: #cc6666;
}
.ace-tomorrow-night .ace_comment {
color: #969896;
}
.ace-tomorrow-night .ace_indent-guide {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC)
right repeat-y;
}
.ace-tomorrow-night .ace_indent-guide-active {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQIW2PQ1dX9zzBz5sz/ABCcBFFentLlAAAAAElFTkSuQmCC)
right repeat-y;
}

View File

@ -0,0 +1,83 @@
.jsoneditor {
border: none;
color: rgb(var(--text-primary));
overflow: auto;
scrollbar-width: none;
background-color: var(--bg-base);
.jsoneditor-menu {
background-color: var(--bg-base);
// border-color: var(--border-button);
border-bottom: thin solid var(--border-button);
}
.jsoneditor-navigation-bar {
border-bottom: 1px solid var(--border-button);
background-color: var(--bg-input);
}
.jsoneditor-tree {
background: var(--bg-base);
}
.jsoneditor-highlight {
background-color: var(--bg-card);
}
}
.jsoneditor-popover,
.jsoneditor-schema-error,
div.jsoneditor td,
div.jsoneditor textarea,
div.jsoneditor th,
div.jsoneditor-field,
div.jsoneditor-value,
pre.jsoneditor-preview {
font-family: consolas, menlo, monaco, 'Ubuntu Mono', source-code-pro,
monospace;
font-size: 14px;
color: rgb(var(--text-primary));
}
div.jsoneditor-field.jsoneditor-highlight,
div.jsoneditor-field[contenteditable='true']:focus,
div.jsoneditor-field[contenteditable='true']:hover,
div.jsoneditor-value.jsoneditor-highlight,
div.jsoneditor-value[contenteditable='true']:focus,
div.jsoneditor-value[contenteditable='true']:hover {
background-color: var(--bg-input);
border: 1px solid var(--border-button);
border-radius: 2px;
}
.jsoneditor-selected,
.jsoneditor-contextmenu .jsoneditor-menu li ul {
background: var(--bg-base);
}
.jsoneditor-contextmenu .jsoneditor-menu button {
color: rgb(var(--text-secondary));
}
.jsoneditor-menu a.jsoneditor-poweredBy {
display: none;
}
.ace-jsoneditor .ace_scroller {
background-color: var(--bg-base);
}
.jsoneditor-statusbar {
border-top: 1px solid var(--border-button);
background-color: var(--bg-base);
color: rgb(var(--text-primary));
}
.jsoneditor-menu > .jsoneditor-modes > button,
.jsoneditor-menu > button {
// color: rgb(var(--text-secondary));
background-color: var(--text-disabled);
}
.jsoneditor-menu > .jsoneditor-modes > button:active,
.jsoneditor-menu > .jsoneditor-modes > button:focus,
.jsoneditor-menu > button:active,
.jsoneditor-menu > button:focus {
background-color: rgb(var(--text-secondary));
}
.jsoneditor-menu > .jsoneditor-modes > button:hover,
.jsoneditor-menu > button:hover {
background-color: rgb(var(--text-secondary));
border: 1px solid var(--border-button);
}

View File

@ -0,0 +1,142 @@
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import './css/cloud9_night.less';
import './css/index.less';
import { JsonEditorOptions, JsonEditorProps } from './interface';
const defaultConfig: JsonEditorOptions = {
mode: 'code',
modes: ['tree', 'code'],
history: false,
search: false,
mainMenuBar: false,
navigationBar: false,
enableSort: false,
enableTransform: false,
indentation: 2,
};
const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
height = '400px',
className = '',
options = {},
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<any>(null);
const { i18n } = useTranslation();
const currentLanguageRef = useRef<string>(i18n.language);
useEffect(() => {
if (typeof window !== 'undefined') {
const JSONEditor = require('jsoneditor');
import('jsoneditor/dist/jsoneditor.min.css');
if (containerRef.current) {
// Default configuration options
const defaultOptions: JsonEditorOptions = {
...defaultConfig,
language: i18n.language === 'zh' ? 'zh-CN' : 'en',
onChange: () => {
if (editorRef.current && onChange) {
try {
const updatedJson = editorRef.current.get();
onChange(updatedJson);
} catch (err) {
// Do not trigger onChange when parsing error occurs
console.error(err);
}
}
},
...options, // Merge user provided options with defaults
};
editorRef.current = new JSONEditor(
containerRef.current,
defaultOptions,
);
if (value) {
editorRef.current.set(value);
}
}
}
return () => {
if (editorRef.current) {
if (typeof editorRef.current.destroy === 'function') {
editorRef.current.destroy();
}
editorRef.current = null;
}
};
}, []);
useEffect(() => {
// Update language when i18n language changes
// Since JSONEditor doesn't have a setOptions method, we need to recreate the editor
if (editorRef.current && currentLanguageRef.current !== i18n.language) {
currentLanguageRef.current = i18n.language;
// Save current data
let currentData;
try {
currentData = editorRef.current.get();
} catch (e) {
// If there's an error getting data, use the passed value or empty object
currentData = value || {};
}
// Destroy the current editor
if (typeof editorRef.current.destroy === 'function') {
editorRef.current.destroy();
}
// Recreate the editor with new language
const JSONEditor = require('jsoneditor');
const newOptions: JsonEditorOptions = {
...defaultConfig,
language: i18n.language === 'zh' ? 'zh-CN' : 'en',
onChange: () => {
if (editorRef.current && onChange) {
try {
const updatedJson = editorRef.current.get();
onChange(updatedJson);
} catch (err) {
// Do not trigger onChange when parsing error occurs
}
}
},
...options, // Merge user provided options with defaults
};
editorRef.current = new JSONEditor(containerRef.current, newOptions);
editorRef.current.set(currentData);
}
}, [i18n.language, value, onChange, options]);
useEffect(() => {
if (editorRef.current && value !== undefined) {
try {
// Only update the editor when the value actually changes
const currentJson = editorRef.current.get();
if (JSON.stringify(currentJson) !== JSON.stringify(value)) {
editorRef.current.set(value);
}
} catch (err) {
// Skip update if there is a syntax error in the current editor
editorRef.current.set(value);
}
}
}, [value]);
return (
<div
ref={containerRef}
style={{ height }}
className={`ace-tomorrow-night w-full border border-border-button rounded-lg overflow-hidden bg-bg-input ${className} `}
/>
);
};
export default JsonEditor;

View File

@ -0,0 +1,339 @@
// JSONEditor configuration options interface see: https://github.com/josdejong/jsoneditor/blob/master/docs/api.md
export interface JsonEditorOptions {
/**
* Editor mode. Available values: 'tree' (default), 'view', 'form', 'text', and 'code'.
*/
mode?: 'tree' | 'view' | 'form' | 'text' | 'code';
/**
* Array of available modes
*/
modes?: Array<'tree' | 'view' | 'form' | 'text' | 'code'>;
/**
* Field name for the root node. Only applicable for modes 'tree', 'view', and 'form'
*/
name?: string;
/**
* Theme for the editor
*/
theme?: string;
/**
* Enable history (undo/redo). True by default. Only applicable for modes 'tree', 'view', and 'form'
*/
history?: boolean;
/**
* Enable search box. True by default. Only applicable for modes 'tree', 'view', and 'form'
*/
search?: boolean;
/**
* Main menu bar visibility
*/
mainMenuBar?: boolean;
/**
* Navigation bar visibility
*/
navigationBar?: boolean;
/**
* Status bar visibility
*/
statusBar?: boolean;
/**
* If true, object keys are sorted before display. false by default.
*/
sortObjectKeys?: boolean;
/**
* Enable transform functionality
*/
enableTransform?: boolean;
/**
* Enable sort functionality
*/
enableSort?: boolean;
/**
* Limit dragging functionality
*/
limitDragging?: boolean;
/**
* A JSON schema object
*/
schema?: any;
/**
* Schemas that are referenced using the `$ref` property from the JSON schema
*/
schemaRefs?: Record<string, any>;
/**
* Array of template objects
*/
templates?: Array<{
text: string;
title?: string;
className?: string;
field?: string;
value: any;
}>;
/**
* Ace editor instance
*/
ace?: any;
/**
* An instance of Ajv JSON schema validator
*/
ajv?: any;
/**
* Switch to enable/disable autocomplete
*/
autocomplete?: {
confirmKey?: string | string[];
caseSensitive?: boolean;
getOptions?: (
text: string,
path: Array<string | number>,
input: string,
editor: any,
) => string[] | Promise<string[]> | null;
};
/**
* Number of indentation spaces. 4 by default. Only applicable for modes 'text' and 'code'
*/
indentation?: number;
/**
* Available languages
*/
languages?: string[];
/**
* Language of the editor
*/
language?: string;
/**
* Callback method, triggered on change of contents. Does not pass the contents itself.
* See also onChangeJSON and onChangeText.
*/
onChange?: () => void;
/**
* Callback method, triggered in modes on change of contents, passing the changed contents as JSON.
* Only applicable for modes 'tree', 'view', and 'form'.
*/
onChangeJSON?: (json: any) => void;
/**
* Callback method, triggered in modes on change of contents, passing the changed contents as stringified JSON.
*/
onChangeText?: (text: string) => void;
/**
* Callback method, triggered when an error occurs
*/
onError?: (error: Error) => void;
/**
* Callback method, triggered when node is expanded
*/
onExpand?: (node: any) => void;
/**
* Callback method, triggered when node is collapsed
*/
onCollapse?: (node: any) => void;
/**
* Callback method, determines if a node is editable
*/
onEditable?: (node: any) => boolean | { field: boolean; value: boolean };
/**
* Callback method, triggered when an event occurs in a JSON field or value.
* Only applicable for modes 'form', 'tree' and 'view'
*/
onEvent?: (node: any, event: Event) => void;
/**
* Callback method, triggered when the editor comes into focus, passing an object {type, target}.
* Applicable for all modes
*/
onFocus?: (node: any) => void;
/**
* Callback method, triggered when the editor goes out of focus, passing an object {type, target}.
* Applicable for all modes
*/
onBlur?: (node: any) => void;
/**
* Callback method, triggered when creating menu items
*/
onCreateMenu?: (menuItems: any[], node: any) => any[];
/**
* Callback method, triggered on node selection change. Only applicable for modes 'tree', 'view', and 'form'
*/
onSelectionChange?: (selection: any) => void;
/**
* Callback method, triggered on text selection change. Only applicable for modes 'text' and 'code'
*/
onTextSelectionChange?: (selection: any) => void;
/**
* Callback method, triggered when a Node DOM is rendered. Function returns a css class name to be set on a node.
* Only applicable for modes 'form', 'tree' and 'view'
*/
onClassName?: (node: any) => string | undefined;
/**
* Callback method, triggered when validating nodes
*/
onValidate?: (
json: any,
) =>
| Array<{ path: Array<string | number>; message: string }>
| Promise<Array<{ path: Array<string | number>; message: string }>>;
/**
* Callback method, triggered when node name is determined
*/
onNodeName?: (parentNode: any, childNode: any, name: string) => string;
/**
* Callback method, triggered when mode changes
*/
onModeChange?: (newMode: string, oldMode: string) => void;
/**
* Color picker options
*/
colorPicker?: boolean;
/**
* Callback method for color picker
*/
onColorPicker?: (
callback: (color: string) => void,
parent: HTMLElement,
) => void;
/**
* If true, shows timestamp tag
*/
timestampTag?: boolean;
/**
* Format for timestamps
*/
timestampFormat?: string;
/**
* If true, unicode characters are escaped. false by default.
*/
escapeUnicode?: boolean;
/**
* Number of children allowed for a node in 'tree', 'view', or 'form' mode before
* the "show more/show all" buttons appear. 100 by default.
*/
maxVisibleChilds?: number;
/**
* Callback method for validation errors
*/
onValidationError?: (
errors: Array<{ path: Array<string | number>; message: string }>,
) => void;
/**
* Callback method for validation warnings
*/
onValidationWarning?: (
warnings: Array<{ path: Array<string | number>; message: string }>,
) => void;
/**
* The anchor element to apply an overlay and display the modals in a centered location. Defaults to document.body
*/
modalAnchor?: HTMLElement | null;
/**
* Anchor element for popups
*/
popupAnchor?: HTMLElement | null;
/**
* Function to create queries
*/
createQuery?: () => void;
/**
* Function to execute queries
*/
executeQuery?: () => void;
/**
* Query description
*/
queryDescription?: string;
/**
* Allow schema suggestions
*/
allowSchemaSuggestions?: boolean;
/**
* Show error table
*/
showErrorTable?: boolean;
/**
* Validate current JSON object against the configured JSON schema
* Must be implemented by tree mode and text mode
*/
validate?: () => Promise<any[]>;
/**
* Refresh the rendered contents
* Can be implemented by tree mode and text mode
*/
refresh?: () => void;
/**
* Callback method triggered when schema changes
*/
_onSchemaChange?: (schema: any, schemaRefs: any) => void;
}
export interface JsonEditorProps {
// JSON data to be displayed in the editor
value?: any;
// Callback function triggered when the JSON data changes
onChange?: (value: any) => void;
// Height of the editor
height?: string;
// Additional CSS class names
className?: string;
// Configuration options for the JSONEditor
options?: JsonEditorOptions;
}

View File

@ -288,11 +288,12 @@ export default {
baseInfo: 'Основная информация',
globalIndex: 'Глобальный индекс',
dataSource: 'Источник данных',
linkSourceSetTip: 'Управление связью источника данных с этим набором данных',
linkSourceSetTip:
'Управление связью источника данных с этим набором данных',
linkDataSource: 'Связать источник данных',
tocExtraction: 'Улучшение оглавлением',
tocExtractionTip:
" Для существующих чанков генерирует иерархическое оглавление (одна директория на файл). При запросах, когда активировано Улучшение оглавлением, система будет использовать большую модель для определения, какие элементы оглавления релевантны вопросу пользователя, тем самым идентифицируя релевантные чанки.",
' Для существующих чанков генерирует иерархическое оглавление (одна директория на файл). При запросах, когда активировано Улучшение оглавлением, система будет использовать большую модель для определения, какие элементы оглавления релевантны вопросу пользователя, тем самым идентифицируя релевантные чанки.',
deleteGenerateModalContent: `
<p>Удаление сгенерированных результатов <strong class='text-text-primary'>{{type}}</strong>
удалит все производные сущности и отношения из этого набора данных.
@ -308,7 +309,8 @@ export default {
setDefaultTip: '',
setDefault: 'Установить по умолчанию',
eidtLinkDataPipeline: 'Редактировать пайплайн обработки',
linkPipelineSetTip: 'Управление связью пайплайна обработки с этим набором данных',
linkPipelineSetTip:
'Управление связью пайплайна обработки с этим набором данных',
default: 'По умолчанию',
dataPipeline: 'Пайплайн обработки',
linkDataPipeline: 'Связать пайплайн обработки',
@ -455,7 +457,8 @@ export default {
{cluster_content}
Выше приведено содержимое, которое вам нужно суммировать.`,
maxToken: 'Макс. токенов',
maxTokenTip: 'Максимальное количество токенов на генерируемый суммаризированный чанк.',
maxTokenTip:
'Максимальное количество токенов на генерируемый суммаризированный чанк.',
maxTokenMessage: 'Макс. токенов обязательно',
threshold: 'Порог',
thresholdTip:
@ -611,12 +614,14 @@ export default {
maxTokens: 'Макс. токенов',
maxTokensMessage: 'Макс. токенов обязательно',
maxTokensTip: `Это устанавливает максимальную длину вывода модели, измеряемую в количестве токенов (слов или частей слов). По умолчанию 512. Если отключено, вы снимаете ограничение на максимальное количество токенов, позволяя модели определять количество токенов в своих ответах.`,
maxTokensInvalidMessage: 'Пожалуйста, введите действительное число для Макс. Токенов.',
maxTokensInvalidMessage:
'Пожалуйста, введите действительное число для Макс. Токенов.',
maxTokensMinMessage: 'Макс. Токенов не может быть меньше 0.',
quote: 'Показать цитату',
quoteTip: 'Отображать ли исходный текст в качестве ссылки.',
selfRag: 'Self-RAG',
selfRagTip: 'Пожалуйста, обратитесь к: https://huggingface.co/papers/2310.11511',
selfRagTip:
'Пожалуйста, обратитесь к: https://huggingface.co/papers/2310.11511',
overview: 'ID чата',
pv: 'Количество сообщений',
uv: 'Количество активных пользователей',
@ -762,11 +767,11 @@ export default {
jiraEmailTip: 'Email, связанный с учетной записью/API токеном Jira.',
jiraTokenTip:
'API токен, сгенерированный из https://id.atlassian.com/manage-profile/security/api-tokens.',
jiraPasswordTip:
'Опциональный пароль для сред Jira Server/Data Center.',
jiraPasswordTip: 'Опциональный пароль для сред Jira Server/Data Center.',
availableSourcesDescription: 'Выберите источник данных для добавления',
availableSources: 'Доступные источники',
datasourceDescription: 'Управляйте вашими источниками данных и подключениями',
datasourceDescription:
'Управляйте вашими источниками данных и подключениями',
save: 'Сохранить',
search: 'Поиск',
availableModels: 'Доступные модели',
@ -777,13 +782,15 @@ export default {
maxTokens: 'Макс. Токенов',
maxTokensMessage: 'Макс. Токенов обязательно',
maxTokensTip: `Это устанавливает максимальную длину вывода модели, измеряемую в количестве токенов (слов или частей слов). По умолчанию 512. Если отключено, вы снимаете ограничение на максимальное количество токенов, позволяя модели определять количество токенов в своих ответах.`,
maxTokensInvalidMessage: 'Пожалуйста, введите действительное число для Макс. Токенов.',
maxTokensInvalidMessage:
'Пожалуйста, введите действительное число для Макс. Токенов.',
maxTokensMinMessage: 'Макс. Токенов не может быть меньше 0.',
password: 'Пароль',
passwordDescription:
'Пожалуйста, введите ваш текущий пароль, чтобы изменить ваш пароль.',
model: 'Провайдеры моделей',
systemModelDescription: 'Пожалуйста, завершите эти настройки перед началом',
systemModelDescription:
'Пожалуйста, завершите эти настройки перед началом',
dataSources: 'Источники данных',
team: 'Команда',
system: 'Система',
@ -829,7 +836,8 @@ export default {
'Если ваш API ключ от OpenAI, просто проигнорируйте это. Любые другие промежуточные провайдеры дадут этот базовый url вместе с API ключом.',
tongyiBaseUrlTip:
'Для китайских пользователей не нужно заполнять или используйте https://dashscope.aliyuncs.com/compatible-mode/v1. Для международных пользователей используйте https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
tongyiBaseUrlPlaceholder: '(Только для международных пользователей, см. подсказку)',
tongyiBaseUrlPlaceholder:
'(Только для международных пользователей, см. подсказку)',
modify: 'Изменить',
systemModelSettings: 'Установить модели по умолчанию',
chatModel: 'LLM',
@ -963,7 +971,8 @@ export default {
joinedTeams: 'Присоединенные команды',
sureDelete: 'Вы уверены, что хотите удалить этого участника?',
quit: 'Выйти',
sureQuit: 'Вы уверены, что хотите выйти из команды, к которой присоединились?',
sureQuit:
'Вы уверены, что хотите выйти из команды, к которой присоединились?',
secretKey: 'Секретный ключ',
publicKey: 'Публичный ключ',
secretKeyMessage: 'Пожалуйста, введите секретный ключ',
@ -972,7 +981,7 @@ export default {
configuration: 'Конфигурация',
langfuseDescription:
'Трассировки, оценки, управление промптами и метрики для отладки и улучшения вашего LLM приложения.',
viewLangfuseSDocumentation: "Посмотреть документацию Langfuse",
viewLangfuseSDocumentation: 'Посмотреть документацию Langfuse',
view: 'Просмотр',
modelsToBeAddedTooltip:
'Если ваш провайдер моделей не указан, но заявляет о "совместимости с OpenAI-API", выберите карточку OpenAI-API-compatible, чтобы добавить соответствующие модели. ',
@ -1093,8 +1102,7 @@ export default {
'Представляет текущий элемент в итерации, который может быть использован и обработан на последующих шагах.',
guidingQuestion: 'Направляющий вопрос',
onFailure: 'При неудаче',
userPromptDefaultValue:
'Это заказ, который вам нужно отправить агенту.',
userPromptDefaultValue: 'Это заказ, который вам нужно отправить агенту.',
search: 'Поиск',
communication: 'Коммуникация',
developer: 'Разработчик',
@ -1123,7 +1131,8 @@ export default {
multimodalModels: 'Мультимодальные модели',
textOnlyModels: 'Только текстовые модели',
allModels: 'Все модели',
codeExecDescription: 'Напишите вашу пользовательскую логику на Python или Javascript.',
codeExecDescription:
'Напишите вашу пользовательскую логику на Python или Javascript.',
stringTransformDescription:
'Изменяет текстовое содержимое. В настоящее время поддерживает: Разделение или объединение текста.',
foundation: 'Основа',
@ -1598,7 +1607,8 @@ export default {
upload: 'Загрузить',
photo: 'Фото',
permissions: 'Права доступа',
permissionsTip: 'Вы можете установить права доступа участников команды здесь.',
permissionsTip:
'Вы можете установить права доступа участников команды здесь.',
me: 'я',
team: 'Команда',
},
@ -1621,9 +1631,11 @@ export default {
'Выберите базы знаний для связи с этим чат-ассистентом, или выберите переменные, содержащие ID баз знаний ниже.',
knowledgeBaseVars: 'Переменные базы знаний',
code: 'Код',
codeDescription: 'Позволяет разработчикам писать пользовательскую логику на Python.',
codeDescription:
'Позволяет разработчикам писать пользовательскую логику на Python.',
dataOperations: 'Операции с данными',
dataOperationsDescription: 'Выполнять различные операции над объектом Data.',
dataOperationsDescription:
'Выполнять различные операции над объектом Data.',
listOperations: 'Операции со списками',
listOperationsDescription: 'Выполнять операции над списком.',
variableAssigner: 'Назначитель переменных',
@ -2056,7 +2068,8 @@ export default {
isSuperuser: 'Суперпользователь',
deleteUser: 'Удалить пользователя',
deleteUserConfirmation: 'Вы уверены, что хотите удалить этого пользователя?',
deleteUserConfirmation:
'Вы уверены, что хотите удалить этого пользователя?',
createNewUser: 'Создать нового пользователя',
changePassword: 'Изменить пароль',
@ -2065,7 +2078,8 @@ export default {
password: 'Пароль',
confirmPassword: 'Подтвердите пароль',
invalidEmail: 'Пожалуйста, введите действительный адрес электронной почты!',
invalidEmail:
'Пожалуйста, введите действительный адрес электронной почты!',
passwordRequired: 'Пожалуйста, введите ваш пароль!',
passwordMinLength: 'Пароль должен быть длиннее 8 символов.',
confirmPasswordRequired: 'Пожалуйста, подтвердите ваш пароль!',

View File

@ -1,7 +1,7 @@
import JsonEditor from '@/components/json-edit';
import { BlockButton, Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Segmented } from '@/components/ui/segmented';
import { Editor } from '@monaco-editor/react';
import { t } from 'i18next';
import { Trash2, X } from 'lucide-react';
import { useCallback } from 'react';
@ -31,32 +31,80 @@ export const useObjectFields = () => {
},
[],
);
const validateKeys = (
obj: any,
path: (string | number)[] = [],
): Array<{ path: (string | number)[]; message: string }> => {
const errors: Array<{ path: (string | number)[]; message: string }> = [];
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (!/^[a-zA-Z_]+$/.test(key)) {
errors.push({
path: [...path, key],
message: `Key "${key}" is invalid. Keys can only contain letters and underscores.`,
});
}
const nestedErrors = validateKeys(obj[key], [...path, key]);
errors.push(...nestedErrors);
}
}
} else if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const nestedErrors = validateKeys(item, [...path, index]);
errors.push(...nestedErrors);
});
}
return errors;
};
const objectRender = useCallback((field: FieldValues) => {
const fieldValue =
typeof field.value === 'object'
? JSON.stringify(field.value, null, 2)
: JSON.stringify({}, null, 2);
console.log('object-render-field', field, fieldValue);
// const fieldValue =
// typeof field.value === 'object'
// ? JSON.stringify(field.value, null, 2)
// : JSON.stringify({}, null, 2);
// console.log('object-render-field', field, fieldValue);
return (
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
value={fieldValue}
// <Editor
// height={200}
// defaultLanguage="json"
// theme="vs-dark"
// value={fieldValue}
// onChange={field.onChange}
// />
<JsonEditor
value={field.value}
onChange={field.onChange}
height="400px"
options={{
mode: 'code',
navigationBar: false,
mainMenuBar: true,
history: true,
onValidate: (json) => {
return validateKeys(json);
},
}}
/>
);
}, []);
const objectValidate = useCallback((value: any) => {
try {
if (!JSON.parse(value)) {
throw new Error(t('knowledgeDetails.formatTypeError'));
if (validateKeys(value, [])?.length > 0) {
throw new Error(t('flow.formatTypeError'));
}
if (!z.object({}).safeParse(value).success) {
throw new Error(t('flow.formatTypeError'));
}
if (value && typeof value === 'string' && !JSON.parse(value)) {
throw new Error(t('flow.formatTypeError'));
}
return true;
} catch (e) {
throw new Error(t('knowledgeDetails.formatTypeError'));
console.log('object-render-error', e, value);
throw new Error(t('flow.formatTypeError'));
}
}, []);
@ -219,6 +267,10 @@ export const useObjectFields = () => {
};
const handleCustomSchema = (value: TypesWithArray) => {
switch (value) {
case TypesWithArray.Object:
return z.object({});
case TypesWithArray.ArrayObject:
return z.array(z.object({}));
case TypesWithArray.ArrayString:
return z.array(z.string());
case TypesWithArray.ArrayNumber: