mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-15 21:35:53 +08:00
feat: add AJV for JSON schema validation and improve error handling
This commit is contained in:
parent
80a928a7b1
commit
44be94d5b5
@ -0,0 +1,133 @@
|
||||
import React, { type FC, useCallback, useEffect, useRef } from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { Editor } from '@monaco-editor/react'
|
||||
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
type CodeEditorProps = {
|
||||
value: string
|
||||
onUpdate?: (value: string) => void
|
||||
showFormatButton?: boolean
|
||||
editorWrapperClassName?: string
|
||||
readOnly?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const CodeEditor: FC<CodeEditorProps> = ({
|
||||
value,
|
||||
onUpdate,
|
||||
showFormatButton = true,
|
||||
editorWrapperClassName,
|
||||
readOnly = false,
|
||||
className,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const monacoRef = useRef<any>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (monacoRef.current) {
|
||||
if (theme === Theme.light)
|
||||
monacoRef.current.editor.setTheme('light-theme')
|
||||
else
|
||||
monacoRef.current.editor.setTheme('dark-theme')
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('light-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000000',
|
||||
'focusBorder': '#00000000',
|
||||
},
|
||||
})
|
||||
monaco.editor.defineTheme('dark-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000000',
|
||||
'focusBorder': '#00000000',
|
||||
},
|
||||
})
|
||||
monaco.editor.setTheme('light-theme')
|
||||
}, [])
|
||||
|
||||
const formatJsonContent = useCallback(() => {
|
||||
if (editorRef.current)
|
||||
editorRef.current.getAction('editor.action.formatDocument')?.run()
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||
if (value)
|
||||
onUpdate?.(value)
|
||||
}, [onUpdate])
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', className)}>
|
||||
<div className='flex items-center justify-between pl-2 pt-1 pr-1'>
|
||||
<div className='py-0.5 text-text-secondary system-xs-semibold-uppercase'>
|
||||
<span className='px-1 py-0.5'>JSON</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
{showFormatButton && (
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={formatJsonContent}
|
||||
>
|
||||
<RiIndentIncrease className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={() => copy(value)}>
|
||||
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('relative', editorWrapperClassName)}>
|
||||
<Editor
|
||||
height='100%'
|
||||
defaultLanguage='json'
|
||||
value={value}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: false },
|
||||
tabSize: 2,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
// Add these options
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
renderLineHighlight: 'none',
|
||||
// Hide scrollbar borders
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CodeEditor)
|
@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { RiErrorWarningFill } from '@remixicon/react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type ErrorMessageProps = {
|
||||
message: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const ErrorMessage: FC<ErrorMessageProps> = ({
|
||||
message,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'flex gap-x-1 mt-1 p-2 rounded-lg border-[0.5px] border-components-panel-border bg-toast-error-bg',
|
||||
className,
|
||||
)}>
|
||||
<RiErrorWarningFill className='shrink-0 w-4 h-4 text-text-destructive' />
|
||||
<div className='grow text-text-primary system-xs-medium max-h-12 overflow-y-auto break-words'>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ErrorMessage)
|
@ -2,12 +2,12 @@ import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiClipboardLine, RiCloseLine, RiErrorWarningFill, RiIndentIncrease } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { Editor } from '@monaco-editor/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { checkDepth } from '../../utils'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import CodeEditor from './code-editor'
|
||||
import ErrorMessage from './error-message'
|
||||
|
||||
type JsonImporterProps = {
|
||||
onSubmit: (schema: string) => void
|
||||
@ -23,8 +23,6 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
||||
const [json, setJson] = useState('')
|
||||
const [parseError, setParseError] = useState<any>(null)
|
||||
const importBtnRef = useRef<HTMLButtonElement>(null)
|
||||
const monacoRef = useRef<any>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (importBtnRef.current) {
|
||||
@ -34,28 +32,6 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('light-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000000',
|
||||
'focusBorder': '#00000000',
|
||||
},
|
||||
})
|
||||
monaco.editor.setTheme('light-theme')
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||
if (!value)
|
||||
return
|
||||
setJson(value)
|
||||
}, [])
|
||||
|
||||
const handleTrigger = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
setOpen(!open)
|
||||
@ -65,11 +41,6 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const formatJsonContent = useCallback(() => {
|
||||
if (editorRef.current)
|
||||
editorRef.current.getAction('editor.action.formatDocument')?.run()
|
||||
}, [])
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
try {
|
||||
const parsedJSON = JSON.parse(json)
|
||||
@ -127,67 +98,14 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className='px-4 py-2'>
|
||||
<div className='flex flex-col h-full rounded-lg bg-components-input-bg-normal overflow-hidden'>
|
||||
<div className='flex items-center justify-between pl-2 pt-1 pr-1'>
|
||||
<div className='py-0.5 text-text-secondary system-xs-semibold-uppercase'>
|
||||
<span className='px-1 py-0.5'>JSON</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={formatJsonContent}
|
||||
>
|
||||
<RiIndentIncrease className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={() => copy(json)}>
|
||||
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative h-[340px]'>
|
||||
<Editor
|
||||
height='100%'
|
||||
defaultLanguage='json'
|
||||
value={json}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly: false,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: false },
|
||||
tabSize: 2,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
// Add these options
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
renderLineHighlight: 'none',
|
||||
// Hide scrollbar borders
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{parseError && (
|
||||
<div className='flex gap-x-1 mt-1 p-2 rounded-lg border-[0.5px] border-components-panel-border bg-toast-error-bg'>
|
||||
<RiErrorWarningFill className='shrink-0 w-4 h-4 text-text-destructive' />
|
||||
<div className='grow text-text-primary system-xs-medium'>
|
||||
{parseError.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CodeEditor
|
||||
className='rounded-lg'
|
||||
editorWrapperClassName='h-[340px]'
|
||||
value={json}
|
||||
onUpdate={setJson}
|
||||
showFormatButton={false}
|
||||
/>
|
||||
{parseError && <ErrorMessage message={parseError.message} />}
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className='flex items-center justify-end gap-x-2 p-4 pt-2'>
|
||||
|
@ -9,8 +9,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import VisualEditor from './visual-editor'
|
||||
import SchemaEditor from './schema-editor'
|
||||
import { jsonToSchema } from '../../utils'
|
||||
import { getValidationErrorMessage, jsonToSchema, validateSchemaAgainstDraft7 } from '../../utils'
|
||||
import { MittProvider, VisualEditorContextProvider } from './visual-editor/context'
|
||||
import ErrorMessage from './error-message'
|
||||
|
||||
type JsonSchemaConfigProps = {
|
||||
defaultSchema?: SchemaRoot
|
||||
@ -45,11 +46,45 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA)
|
||||
const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2))
|
||||
const [btnWidth, setBtnWidth] = useState(0)
|
||||
const [parseError, setParseError] = useState<Error | null>(null)
|
||||
const [validationError, setValidationError] = useState<string>('')
|
||||
|
||||
const updateBtnWidth = useCallback((width: number) => {
|
||||
setBtnWidth(width + 32)
|
||||
}, [])
|
||||
|
||||
const handleTabChange = useCallback((value: SchemaView) => {
|
||||
if (currentTab === value) return
|
||||
if (currentTab === SchemaView.JsonSchema) {
|
||||
try {
|
||||
const schema = JSON.parse(json)
|
||||
setParseError(null)
|
||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
||||
if (ajvError.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(ajvError))
|
||||
return
|
||||
}
|
||||
else {
|
||||
setJsonSchema(schema)
|
||||
setValidationError('')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
setValidationError('')
|
||||
if (error instanceof Error)
|
||||
setParseError(error)
|
||||
else
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return
|
||||
}
|
||||
}
|
||||
else if (currentTab === SchemaView.VisualEditor) {
|
||||
setJson(JSON.stringify(jsonSchema, null, 2))
|
||||
}
|
||||
|
||||
setCurrentTab(value)
|
||||
}, [currentTab, jsonSchema, json])
|
||||
|
||||
const handleApplySchema = useCallback((schema: SchemaRoot) => {
|
||||
setJsonSchema(schema)
|
||||
}, [])
|
||||
@ -69,6 +104,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
|
||||
const handleResetDefaults = useCallback(() => {
|
||||
setJsonSchema(defaultSchema || DEFAULT_SCHEMA)
|
||||
setJson(JSON.stringify(defaultSchema || DEFAULT_SCHEMA, null, 2))
|
||||
}, [defaultSchema])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
@ -76,9 +112,33 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
}, [onClose])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(jsonSchema)
|
||||
let schema = jsonSchema
|
||||
if (currentTab === SchemaView.JsonSchema) {
|
||||
try {
|
||||
schema = JSON.parse(json)
|
||||
setParseError(null)
|
||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
||||
if (ajvError.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(ajvError))
|
||||
return
|
||||
}
|
||||
else {
|
||||
setJsonSchema(schema)
|
||||
setValidationError('')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
setValidationError('')
|
||||
if (error instanceof Error)
|
||||
setParseError(error)
|
||||
else
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return
|
||||
}
|
||||
}
|
||||
onSave(schema)
|
||||
onClose()
|
||||
}, [jsonSchema, onSave, onClose])
|
||||
}, [currentTab, jsonSchema, json, onSave, onClose])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full'>
|
||||
@ -97,9 +157,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
<SegmentedControl<SchemaView>
|
||||
options={VIEW_TABS}
|
||||
value={currentTab}
|
||||
onChange={(value: SchemaView) => {
|
||||
setCurrentTab(value)
|
||||
}}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
{/* JSON Schema Generator */}
|
||||
@ -115,7 +173,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 grow overflow-hidden'>
|
||||
<div className='flex flex-col gap-y-1 px-6 grow overflow-hidden'>
|
||||
{currentTab === SchemaView.VisualEditor && (
|
||||
<MittProvider>
|
||||
<VisualEditorContextProvider>
|
||||
@ -132,6 +190,8 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
onUpdate={handleSchemaEditorUpdate}
|
||||
/>
|
||||
)}
|
||||
{parseError && <ErrorMessage message={parseError.message} />}
|
||||
{validationError && <ErrorMessage message={validationError} />}
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className='flex items-center p-6 pt-5 gap-x-2'>
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React, { type FC, useCallback, useRef, useState } from 'react'
|
||||
import React, { type FC, useCallback, useMemo, useState } from 'react'
|
||||
import type { SchemaRoot } from '../../../types'
|
||||
import { RiArrowLeftLine, RiClipboardLine, RiCloseLine, RiSparklingLine } from '@remixicon/react'
|
||||
import { RiArrowLeftLine, RiCloseLine, RiSparklingLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Button from '@/app/components/base/button'
|
||||
import CodeEditor from '../code-editor'
|
||||
import ErrorMessage from '../error-message'
|
||||
import { getValidationErrorMessage, validateSchemaAgainstDraft7 } from '../../../utils'
|
||||
|
||||
type GeneratedResultProps = {
|
||||
schema: SchemaRoot
|
||||
onBack: () => void
|
||||
onRegenerate: () => void
|
||||
onClose: () => void
|
||||
onApply: (schema: any) => void
|
||||
onApply: () => void
|
||||
}
|
||||
|
||||
const GeneratedResult: FC<GeneratedResultProps> = ({
|
||||
@ -22,57 +23,36 @@ const GeneratedResult: FC<GeneratedResultProps> = ({
|
||||
onApply,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const monacoRef = useRef<any>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
const [parseError, setParseError] = useState<Error | null>(null)
|
||||
const [validationError, setValidationError] = useState<string>('')
|
||||
|
||||
const formatJSON = (json: any): string => {
|
||||
const formatJSON = (json: SchemaRoot) => {
|
||||
try {
|
||||
if (typeof json === 'string') {
|
||||
const parsed = JSON.parse(json)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
}
|
||||
return JSON.stringify(json, null, 2)
|
||||
const schema = JSON.stringify(json, null, 2)
|
||||
setParseError(null)
|
||||
return schema
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to format JSON:', e)
|
||||
return typeof json === 'string' ? json : JSON.stringify(json)
|
||||
if (e instanceof Error)
|
||||
setParseError(e)
|
||||
else
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const [jsonSchema, setJsonSchema] = useState(formatJSON(schema))
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('light-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000000',
|
||||
'focusBorder': '#00000000',
|
||||
},
|
||||
})
|
||||
monaco.editor.setTheme('light-theme')
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||
if (!value)
|
||||
return
|
||||
setJsonSchema(value)
|
||||
}, [])
|
||||
const jsonSchema = useMemo(() => formatJSON(schema), [schema])
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
try {
|
||||
// Parse the JSON to ensure it's valid before applying
|
||||
const parsedJSON = JSON.parse(jsonSchema)
|
||||
onApply(parsedJSON)
|
||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
||||
if (ajvError.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(ajvError))
|
||||
}
|
||||
catch {
|
||||
// TODO: Handle invalid JSON error
|
||||
else {
|
||||
onApply()
|
||||
setValidationError('')
|
||||
}
|
||||
}, [jsonSchema, onApply])
|
||||
}, [schema, onApply])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-[480px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9'>
|
||||
@ -89,51 +69,16 @@ const GeneratedResult: FC<GeneratedResultProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className='w-full h-[468px] px-4 py-2'>
|
||||
<div className='flex flex-col h-full rounded-lg bg-components-input-bg-normal overflow-hidden'>
|
||||
<div className='flex items-center justify-between pl-2 pt-1 pr-1'>
|
||||
<div className='py-0.5 text-text-secondary system-xs-semibold-uppercase'>
|
||||
<span className='px-1 py-0.5'>JSON</span>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={() => copy(jsonSchema)}>
|
||||
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='relative grow'>
|
||||
<Editor
|
||||
height='100%'
|
||||
defaultLanguage='json'
|
||||
value={jsonSchema}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: false },
|
||||
tabSize: 2,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
// Add these options
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
renderLineHighlight: 'none',
|
||||
// Hide scrollbar borders
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<CodeEditor
|
||||
className='rounded-lg'
|
||||
editorWrapperClassName='h-[424px]'
|
||||
value={jsonSchema}
|
||||
readOnly
|
||||
showFormatButton={false}
|
||||
/>
|
||||
{parseError && <ErrorMessage message={parseError.message} />}
|
||||
{validationError && <ErrorMessage message={validationError} />}
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className='flex items-center justify-between p-4 pt-2'>
|
||||
@ -155,4 +100,4 @@ const GeneratedResult: FC<GeneratedResultProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneratedResult
|
||||
export default React.memo(GeneratedResult)
|
||||
|
@ -83,6 +83,7 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
|
||||
const handleApply = () => {
|
||||
onApply(schema!)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine, RiSparklingFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -85,4 +86,4 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptEditor
|
||||
export default React.memo(PromptEditor)
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Editor } from '@monaco-editor/react'
|
||||
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import React, { type FC, useCallback, useRef } from 'react'
|
||||
import React, { type FC } from 'react'
|
||||
import CodeEditor from './code-editor'
|
||||
|
||||
type SchemaEditorProps = {
|
||||
schema: string
|
||||
@ -12,90 +10,13 @@ const SchemaEditor: FC<SchemaEditorProps> = ({
|
||||
schema,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const monacoRef = useRef<any>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('light-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000000',
|
||||
'focusBorder': '#00000000',
|
||||
},
|
||||
})
|
||||
monaco.editor.setTheme('light-theme')
|
||||
}, [])
|
||||
|
||||
const formatJsonContent = useCallback(() => {
|
||||
if (editorRef.current)
|
||||
editorRef.current.getAction('editor.action.formatDocument')?.run()
|
||||
}, [])
|
||||
|
||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||
if (!value)
|
||||
return
|
||||
onUpdate(value)
|
||||
}, [onUpdate])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full rounded-xl bg-components-input-bg-normal overflow-hidden'>
|
||||
<div className='flex items-center justify-between pl-2 pt-1 pr-1'>
|
||||
<div className='py-0.5 text-text-secondary system-xs-semibold-uppercase'>
|
||||
<span className='px-1 py-0.5'>JSON</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={formatJsonContent}
|
||||
>
|
||||
<RiIndentIncrease className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center h-6 w-6'
|
||||
onClick={() => copy(schema)}>
|
||||
<RiClipboardLine className='w-4 h-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative grow'>
|
||||
<Editor
|
||||
height='100%'
|
||||
defaultLanguage='json'
|
||||
value={schema}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
readOnly: false,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: false },
|
||||
tabSize: 2,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'same',
|
||||
// Add these options
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
renderLineHighlight: 'none',
|
||||
// Hide scrollbar borders
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className='rounded-xl'
|
||||
editorWrapperClassName='grow'
|
||||
value={schema}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ArrayType, Type } from './types'
|
||||
import type { ArrayItems, Field, LLMNodeType } from './types'
|
||||
import Ajv, { type ErrorObject } from 'ajv'
|
||||
import draft7MetaSchema from 'ajv/dist/refs/json-schema-draft-07.json'
|
||||
|
||||
export const checkNodeValid = (payload: LLMNodeType) => {
|
||||
return true
|
||||
@ -79,3 +81,28 @@ export const findPropertyWithPath = (target: any, path: string[]) => {
|
||||
current = current[key]
|
||||
return current
|
||||
}
|
||||
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
validateSchema: true,
|
||||
meta: false,
|
||||
})
|
||||
ajv.addMetaSchema(draft7MetaSchema)
|
||||
|
||||
export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
|
||||
// Make sure the schema has the $schema property for draft-07
|
||||
if (!schemaToValidate.$schema)
|
||||
schemaToValidate.$schema = 'http://json-schema.org/draft-07/schema#'
|
||||
|
||||
const valid = ajv.validateSchema(schemaToValidate)
|
||||
|
||||
return valid ? [] : ajv.errors || []
|
||||
}
|
||||
|
||||
export const getValidationErrorMessage = (errors: ErrorObject[]) => {
|
||||
const message = errors.map((error) => {
|
||||
return `Error: ${error.instancePath} ${error.message} Details: ${JSON.stringify(error.params)}`
|
||||
}).join('; ')
|
||||
return message
|
||||
}
|
||||
|
@ -54,6 +54,7 @@
|
||||
"@tanstack/react-query": "^5.60.5",
|
||||
"@tanstack/react-query-devtools": "^5.60.5",
|
||||
"ahooks": "^3.8.1",
|
||||
"ajv": "^8.17.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
|
3
web/pnpm-lock.yaml
generated
3
web/pnpm-lock.yaml
generated
@ -103,6 +103,9 @@ importers:
|
||||
ahooks:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1(react@18.2.0)
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.17.1
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user