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

View File

@ -1,7 +1,7 @@
import JsonEditor from '@/components/json-edit';
import { BlockButton, Button } from '@/components/ui/button'; import { BlockButton, Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Segmented } from '@/components/ui/segmented'; import { Segmented } from '@/components/ui/segmented';
import { Editor } from '@monaco-editor/react';
import { t } from 'i18next'; import { t } from 'i18next';
import { Trash2, X } from 'lucide-react'; import { Trash2, X } from 'lucide-react';
import { useCallback } from '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 objectRender = useCallback((field: FieldValues) => {
const fieldValue = // const fieldValue =
typeof field.value === 'object' // typeof field.value === 'object'
? JSON.stringify(field.value, null, 2) // ? JSON.stringify(field.value, null, 2)
: JSON.stringify({}, null, 2); // : JSON.stringify({}, null, 2);
console.log('object-render-field', field, fieldValue); // console.log('object-render-field', field, fieldValue);
return ( return (
<Editor // <Editor
height={200} // height={200}
defaultLanguage="json" // defaultLanguage="json"
theme="vs-dark" // theme="vs-dark"
value={fieldValue} // value={fieldValue}
// onChange={field.onChange}
// />
<JsonEditor
value={field.value}
onChange={field.onChange} onChange={field.onChange}
height="400px"
options={{
mode: 'code',
navigationBar: false,
mainMenuBar: true,
history: true,
onValidate: (json) => {
return validateKeys(json);
},
}}
/> />
); );
}, []); }, []);
const objectValidate = useCallback((value: any) => { const objectValidate = useCallback((value: any) => {
try { try {
if (!JSON.parse(value)) { if (validateKeys(value, [])?.length > 0) {
throw new Error(t('knowledgeDetails.formatTypeError')); 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; return true;
} catch (e) { } 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) => { const handleCustomSchema = (value: TypesWithArray) => {
switch (value) { switch (value) {
case TypesWithArray.Object:
return z.object({});
case TypesWithArray.ArrayObject:
return z.array(z.object({}));
case TypesWithArray.ArrayString: case TypesWithArray.ArrayString:
return z.array(z.string()); return z.array(z.string());
case TypesWithArray.ArrayNumber: case TypesWithArray.ArrayNumber: