'use client'; import { useEffect, useState, useRef } from 'react'; import { JobConfig } from '@/types'; import YAML from 'yaml'; import Editor, { OnMount } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; import { Settings } from '@/hooks/useSettings'; type Props = { jobConfig: JobConfig; setJobConfig: (value: any, key?: string) => void; status: 'idle' | 'saving' | 'success' | 'error'; handleSubmit: (event: React.FormEvent) => void; runId: string | null; gpuIDs: string | null; setGpuIDs: (value: string | null) => void; gpuList: any; datasetOptions: any; settings: Settings; }; const isDev = process.env.NODE_ENV === 'development'; const yamlConfig: YAML.DocumentOptions & YAML.SchemaOptions & YAML.ParseOptions & YAML.CreateNodeOptions & YAML.ToStringOptions = { indent: 2, lineWidth: 999999999999, defaultStringType: 'QUOTE_DOUBLE', defaultKeyType: 'PLAIN', directives: true, }; export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props) { const [editorValue, setEditorValue] = useState(''); const lastJobConfigUpdateStringRef = useRef(''); const editorRef = useRef(null); // Track if the editor has been mounted const isEditorMounted = useRef(false); // Handler for editor mounting const handleEditorDidMount: OnMount = editor => { editorRef.current = editor; isEditorMounted.current = true; // Initial content setup try { const yamlContent = YAML.stringify(jobConfig, yamlConfig); setEditorValue(yamlContent); lastJobConfigUpdateStringRef.current = JSON.stringify(jobConfig); } catch (e) { console.warn(e); } }; useEffect(() => { const lastUpdate = lastJobConfigUpdateStringRef.current; const currentUpdate = JSON.stringify(jobConfig); // Skip if no changes or editor not yet mounted if (lastUpdate === currentUpdate || !isEditorMounted.current) { return; } try { // Preserve cursor position and selection const editor = editorRef.current; if (editor) { // Save current editor state const position = editor.getPosition(); const selection = editor.getSelection(); const scrollTop = editor.getScrollTop(); // Update content const yamlContent = YAML.stringify(jobConfig, yamlConfig); // Only update if the content is actually different if (yamlContent !== editor.getValue()) { // Set value directly on the editor model instead of using React state editor.getModel()?.setValue(yamlContent); // Restore cursor position and selection if (position) editor.setPosition(position); if (selection) editor.setSelection(selection); editor.setScrollTop(scrollTop); } lastJobConfigUpdateStringRef.current = currentUpdate; } } catch (e) { console.warn(e); } }, [jobConfig]); const handleChange = (value: string | undefined) => { if (value === undefined) return; try { const parsed = YAML.parse(value); // Don't update jobConfig if the change came from the editor itself // to avoid a circular update loop if (JSON.stringify(parsed) !== lastJobConfigUpdateStringRef.current) { lastJobConfigUpdateStringRef.current = JSON.stringify(parsed); // We have to ensure certain things are always set try { parsed.config.process[0].type = 'ui_trainer'; parsed.config.process[0].sqlite_db_path = './aitk_db.db'; parsed.config.process[0].training_folder = settings.TRAINING_FOLDER; parsed.config.process[0].device = 'cuda'; parsed.config.process[0].performance_log_every = 10; } catch (e) { console.warn(e); } setJobConfig(parsed); } } catch (e) { // Don't update on parsing errors console.warn(e); } }; return ( <> ); }