diff --git a/ui/src/app/jobs/new/AdvancedJob.tsx b/ui/src/app/jobs/new/AdvancedJob.tsx new file mode 100644 index 00000000..9d0873e1 --- /dev/null +++ b/ui/src/app/jobs/new/AdvancedJob.tsx @@ -0,0 +1,148 @@ +'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 ( + <> + + + ); +} diff --git a/ui/src/app/jobs/new/SimpleJob.tsx b/ui/src/app/jobs/new/SimpleJob.tsx new file mode 100644 index 00000000..75bab27d --- /dev/null +++ b/ui/src/app/jobs/new/SimpleJob.tsx @@ -0,0 +1,627 @@ +'use client'; + +import { options, modelArchs, isVideoModelFromArch } from './options'; +import { defaultDatasetConfig } from './jobConfig'; +import { JobConfig } from '@/types'; +import { objectCopy } from '@/utils/basic'; +import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput } from '@/components/formInputs'; +import Card from '@/components/Card'; +import { X } from 'lucide-react'; + +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; +}; + +const isDev = process.env.NODE_ENV === 'development'; + +export default function SimpleJob({ + jobConfig, + setJobConfig, + handleSubmit, + status, + runId, + gpuIDs, + setGpuIDs, + gpuList, + datasetOptions, +}: Props) { + const isVideoModel = isVideoModelFromArch(jobConfig.config.process[0].model.arch); + return ( + <> +
+
+ + setJobConfig(value, 'config.name')} + placeholder="Enter training name" + disabled={runId !== null} + required + /> + setGpuIDs(value)} + options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))} + /> + { + if (value?.trim() === '') { + value = null; + } + setJobConfig(value, 'config.process[0].trigger_word'); + }} + placeholder="" + required + /> + + + {/* Model Configuration Section */} + + { + // see if model changed + const currentModel = options.model.find( + model => model.name_or_path === jobConfig.config.process[0].model.name_or_path, + ); + if (!currentModel || currentModel.name_or_path === value) { + // model has not changed + return; + } + // revert defaults from previous model + for (const key in currentModel.defaults) { + setJobConfig(currentModel.defaults[key][1], key); + } + // set new model + setJobConfig(value, 'config.process[0].model.name_or_path'); + // update the defaults when a model is selected + const model = options.model.find(model => model.name_or_path === value); + if (model?.defaults) { + for (const key in model.defaults) { + setJobConfig(model.defaults[key][0], key); + } + } + }} + options={ + options.model + .map(model => { + if (model.dev_only && !isDev) { + return null; + } + return { + value: model.name_or_path, + label: model.name_or_path, + }; + }) + .filter(x => x) as { value: string; label: string }[] + } + /> + { + const currentArch = modelArchs.find(a => a.name === jobConfig.config.process[0].model.arch); + if (!currentArch || currentArch.name === value) { + return; + } + // set new model + setJobConfig(value, 'config.process[0].model.arch'); + }} + options={ + modelArchs + .map(model => { + return { + value: model.name, + label: model.label, + }; + }) + .filter(x => x) as { value: string; label: string }[] + } + /> + +
+ setJobConfig(value, 'config.process[0].model.quantize')} + /> + setJobConfig(value, 'config.process[0].model.quantize_te')} + /> +
+
+
+ + setJobConfig(value, 'config.process[0].network.type')} + options={[ + { value: 'lora', label: 'LoRA' }, + { value: 'lokr', label: 'LoKr' }, + ]} + /> + {jobConfig.config.process[0].network?.type == 'lokr' && ( + setJobConfig(parseInt(value), 'config.process[0].network.lokr_factor')} + options={[ + { value: '-1', label: 'Auto' }, + { value: '4', label: '4' }, + { value: '8', label: '8' }, + { value: '16', label: '16' }, + { value: '32', label: '32' }, + ]} + /> + )} + {jobConfig.config.process[0].network?.type == 'lora' && ( + { + console.log('onChange', value); + setJobConfig(value, 'config.process[0].network.linear'); + setJobConfig(value, 'config.process[0].network.linear_alpha'); + }} + placeholder="eg. 16" + min={0} + max={1024} + required + /> + )} + + + setJobConfig(value, 'config.process[0].save.dtype')} + options={[ + { value: 'bf16', label: 'BF16' }, + { value: 'fp16', label: 'FP16' }, + { value: 'fp32', label: 'FP32' }, + ]} + /> + setJobConfig(value, 'config.process[0].save.save_every')} + placeholder="eg. 250" + min={1} + required + /> + setJobConfig(value, 'config.process[0].save.max_step_saves_to_keep')} + placeholder="eg. 4" + min={1} + required + /> + +
+
+ +
+
+ setJobConfig(value, 'config.process[0].train.batch_size')} + placeholder="eg. 4" + min={1} + required + /> + setJobConfig(value, 'config.process[0].train.gradient_accumulation')} + placeholder="eg. 1" + min={1} + required + /> + setJobConfig(value, 'config.process[0].train.steps')} + placeholder="eg. 2000" + min={1} + required + /> +
+
+ setJobConfig(value, 'config.process[0].train.optimizer')} + options={[ + { value: 'adamw8bit', label: 'AdamW8Bit' }, + { value: 'adafactor', label: 'Adafactor' }, + ]} + /> + setJobConfig(value, 'config.process[0].train.lr')} + placeholder="eg. 0.0001" + min={0} + required + /> + setJobConfig(value, 'config.process[0].train.optimizer_params.weight_decay')} + placeholder="eg. 0.0001" + min={0} + required + /> +
+
+ setJobConfig(value, 'config.process[0].train.timestep_type')} + options={[ + { value: 'sigmoid', label: 'Sigmoid' }, + { value: 'linear', label: 'Linear' }, + { value: 'flux_shift', label: 'Flux Shift' }, + ]} + /> + setJobConfig(value, 'config.process[0].train.content_or_style')} + options={[ + { value: 'balanced', label: 'Balanced' }, + { value: 'content', label: 'High Noise' }, + { value: 'style', label: 'Low Noise' }, + ]} + /> + setJobConfig(value, 'config.process[0].train.noise_scheduler')} + options={[ + { value: 'flowmatch', label: 'FlowMatch' }, + { value: 'ddpm', label: 'DDPM' }, + ]} + /> +
+
+ + setJobConfig(value, 'config.process[0].train.ema_config.use_ema')} + /> + + setJobConfig(value, 'config.process[0].train.ema_config?.ema_decay')} + placeholder="eg. 0.99" + min={0} + /> + +
+ setJobConfig(value, 'config.process[0].train.unload_text_encoder')} + /> +
+
+
+
+ + setJobConfig(value, 'config.process[0].train.diff_output_preservation')} + /> + + setJobConfig(value, 'config.process[0].train.diff_output_preservation_multiplier')} + placeholder="eg. 1.0" + min={0} + /> + setJobConfig(value, 'config.process[0].train.diff_output_preservation_class')} + placeholder="eg. woman" + /> +
+
+
+
+
+ + <> + {jobConfig.config.process[0].datasets.map((dataset, i) => ( +
+ +

Dataset {i + 1}

+
+
+ setJobConfig(value, `config.process[0].datasets[${i}].folder_path`)} + options={datasetOptions} + /> + setJobConfig(value, `config.process[0].datasets[${i}].network_weight`)} + placeholder="eg. 1.0" + /> +
+
+ setJobConfig(value, `config.process[0].datasets[${i}].default_caption`)} + placeholder="eg. A photo of a cat" + /> + setJobConfig(value, `config.process[0].datasets[${i}].caption_dropout_rate`)} + placeholder="eg. 0.05" + min={0} + required + /> +
+
+ + + setJobConfig(value, `config.process[0].datasets[${i}].cache_latents_to_disk`) + } + /> + setJobConfig(value, `config.process[0].datasets[${i}].is_reg`)} + /> + +
+
+ +
+ {[ + [256, 512, 768], + [1024, 1280, 1536], + ].map(resGroup => ( +
+ {resGroup.map(res => ( + { + const resolutions = dataset.resolution.includes(res) + ? dataset.resolution.filter(r => r !== res) + : [...dataset.resolution, res]; + setJobConfig(resolutions, `config.process[0].datasets[${i}].resolution`); + }} + /> + ))} +
+ ))} +
+
+
+
+
+ ))} + + +
+
+
+ +
+
+ setJobConfig(value, 'config.process[0].sample.sample_every')} + placeholder="eg. 250" + min={1} + required + /> + setJobConfig(value, 'config.process[0].sample.sampler')} + options={[ + { value: 'flowmatch', label: 'FlowMatch' }, + { value: 'ddpm', label: 'DDPM' }, + ]} + /> +
+
+ setJobConfig(value, 'config.process[0].sample.guidance_scale')} + placeholder="eg. 1.0" + min={0} + required + /> + setJobConfig(value, 'config.process[0].sample.sample_steps')} + placeholder="eg. 1" + className="pt-2" + min={1} + required + /> +
+
+ setJobConfig(value, 'config.process[0].sample.width')} + placeholder="eg. 1024" + min={0} + required + /> + setJobConfig(value, 'config.process[0].sample.height')} + placeholder="eg. 1024" + className="pt-2" + min={0} + required + /> +
+ +
+ setJobConfig(value, 'config.process[0].sample.seed')} + placeholder="eg. 0" + min={0} + required + /> + setJobConfig(value, 'config.process[0].sample.walk_seed')} + /> +
+ {isVideoModel && ( +
+ setJobConfig(value, 'config.process[0].sample.num_frames')} + placeholder="eg. 0" + min={0} + required + /> + setJobConfig(value, 'config.process[0].sample.fps')} + placeholder="eg. 0" + min={0} + required + /> +
+ )} +
+ + {jobConfig.config.process[0].sample.prompts.map((prompt, i) => ( +
+
+ setJobConfig(value, `config.process[0].sample.prompts[${i}]`)} + placeholder="Enter prompt" + required + /> +
+
+ +
+
+ ))} + +
+
+
+ + {status === 'success' &&

Training saved successfully!

} + {status === 'error' &&

Error saving training. Please try again.

} +
+ + ); +}