Started doing info bubble docs on the simple ui

This commit is contained in:
Jaret Burkett
2025-06-17 11:00:24 -06:00
parent 595a6f1735
commit ff617fdaea
7 changed files with 201 additions and 26 deletions

View File

@@ -419,6 +419,7 @@ Everything else should work the same including layer targeting.
### June 17, 2024
- Performance optimizations for batch preparation
- Added some docs via a popup for items in the simple ui explaining what settings do. Still a WIP
### June 16, 2024
- Hide control images in the UI when viewing datasets

View File

@@ -47,6 +47,7 @@ export default function SimpleJob({
<TextInput
label="Training Name"
value={jobConfig.config.name}
docKey="config.name"
onChange={value => setJobConfig(value, 'config.name')}
placeholder="Enter training name"
disabled={runId !== null}
@@ -55,12 +56,14 @@ export default function SimpleJob({
<SelectInput
label="GPU ID"
value={`${gpuIDs}`}
docKey="gpuids"
onChange={value => setGpuIDs(value)}
options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))}
/>
<TextInput
label="Trigger Word"
value={jobConfig.config.process[0].trigger_word || ''}
docKey="config.process[0].trigger_word"
onChange={(value: string | null) => {
if (value?.trim() === '') {
value = null;
@@ -120,6 +123,7 @@ export default function SimpleJob({
<TextInput
label="Name or Path"
value={jobConfig.config.process[0].model.name_or_path}
docKey="config.process[0].model.name_or_path"
onChange={(value: string | null) => {
if (value?.trim() === '') {
value = null;
@@ -185,22 +189,20 @@ export default function SimpleJob({
max={1024}
required
/>
{
modelArch?.disableSections?.includes('network.conv') ? null : (
<NumberInput
label="Conv Rank"
value={jobConfig.config.process[0].network.conv}
onChange={value => {
console.log('onChange', value);
setJobConfig(value, 'config.process[0].network.conv');
setJobConfig(value, 'config.process[0].network.conv_alpha');
}}
placeholder="eg. 16"
min={0}
max={1024}
/>
)
}
{modelArch?.disableSections?.includes('network.conv') ? null : (
<NumberInput
label="Conv Rank"
value={jobConfig.config.process[0].network.conv}
onChange={value => {
console.log('onChange', value);
setJobConfig(value, 'config.process[0].network.conv');
setJobConfig(value, 'config.process[0].network.conv_alpha');
}}
placeholder="eg. 16"
min={0}
max={1024}
/>
)}
</>
)}
</Card>

View File

@@ -7,6 +7,7 @@ import ConfirmModal from '@/components/ConfirmModal';
import SampleImageModal from '@/components/SampleImageModal';
import { Suspense } from 'react';
import AuthWrapper from '@/components/AuthWrapper';
import DocModal from '@/components/DocModal';
export const dynamic = 'force-dynamic';
@@ -38,6 +39,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</AuthWrapper>
</ThemeProvider>
<ConfirmModal />
<DocModal />
<SampleImageModal />
</body>
</html>

View File

@@ -0,0 +1,59 @@
'use client';
import { createGlobalState } from 'react-global-hooks';
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
import React from 'react';
import { ConfigDoc } from '@/types';
export const docState = createGlobalState<ConfigDoc | null>(null);
export const openDoc = (doc: ConfigDoc) => {
docState.set({ ...doc });
};
export default function DocModal() {
const [doc, setDoc] = docState.use();
const isOpen = !!doc;
const onClose = () => {
setDoc(null);
};
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-10">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-900/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"
/>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<DialogPanel
transition
className="relative transform overflow-hidden rounded-lg bg-gray-800 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-[50rem] data-closed:sm:translate-y-0 data-closed:sm:scale-95"
>
<div className="bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
<DialogTitle as="h3" className={`text-base font-semibold `}>
{doc?.title || 'Confirm Action'}
</DialogTitle>
<div className="mt-2 text-sm text-gray-200">{doc?.description}</div>
</div>
</div>
</div>
<div className="bg-gray-700 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
data-autofocus
onClick={onClose}
className="mt-3 inline-flex w-full justify-center rounded-md bg-gray-800 px-3 py-2 text-sm font-semibold text-gray-200 hover:bg-gray-800 sm:mt-0 sm:w-auto ring-0"
>
Close
</button>
</div>
</DialogPanel>
</div>
</div>
</Dialog>
);
}

View File

@@ -3,6 +3,10 @@
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import dynamic from 'next/dynamic';
import { CircleHelp } from 'lucide-react';
import { getDoc } from '@/docs';
import { openDoc } from '@/components/DocModal';
const Select = dynamic(() => import('react-select'), { ssr: false });
const labelClasses = 'block text-xs mb-1 mt-2 text-gray-300';
@@ -11,6 +15,7 @@ const inputClasses =
export interface InputProps {
label?: string;
docKey?: string;
className?: string;
placeholder?: string;
required?: boolean;
@@ -24,10 +29,20 @@ export interface TextInputProps extends InputProps {
}
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, onChange, placeholder, required, disabled, type = 'text', className }, ref) => {
({ label, value, onChange, placeholder, required, disabled, type = 'text', className, docKey = null }, ref) => {
const doc = getDoc(docKey);
return (
<div className={classNames(className)}>
{label && <label className={labelClasses}>{label}</label>}
{label && (
<label className={labelClasses}>
{label}{' '}
{doc && (
<div className="inline-block ml-1 text-xs text-gray-500 cursor-pointer" onClick={() => openDoc(doc)}>
<CircleHelp className="inline-block w-4 h-4 cursor-pointer" />
</div>
)}
</label>
)}
<input
ref={ref}
type={type}
@@ -56,7 +71,8 @@ export interface NumberInputProps extends InputProps {
}
export const NumberInput = (props: NumberInputProps) => {
const { label, value, onChange, placeholder, required, min, max } = props;
const { label, value, onChange, placeholder, required, min, max, docKey = null } = props;
const doc = getDoc(docKey);
// Add controlled internal state to properly handle partial inputs
const [inputValue, setInputValue] = React.useState<string | number>(value ?? '');
@@ -68,7 +84,16 @@ export const NumberInput = (props: NumberInputProps) => {
return (
<div className={classNames(props.className)}>
{label && <label className={labelClasses}>{label}</label>}
{label && (
<label className={labelClasses}>
{label}{' '}
{doc && (
<div className="inline-block ml-1 text-xs text-gray-500 cursor-pointer" onClick={() => openDoc(doc)}>
<CircleHelp className="inline-block w-4 h-4 cursor-pointer" />
</div>
)}
</label>
)}
<input
type="number"
value={inputValue}
@@ -120,7 +145,8 @@ export interface SelectInputProps extends InputProps {
}
export const SelectInput = (props: SelectInputProps) => {
const { label, value, onChange, options } = props;
const { label, value, onChange, options, docKey = null } = props;
const doc = getDoc(docKey);
const selectedOption = options.find(option => option.value === value);
return (
<div
@@ -128,7 +154,16 @@ export const SelectInput = (props: SelectInputProps) => {
'opacity-30 cursor-not-allowed': props.disabled,
})}
>
{label && <label className={labelClasses}>{label}</label>}
{label && (
<label className={labelClasses}>
{label}{' '}
{doc && (
<div className="inline-block ml-1 text-xs text-gray-500 cursor-pointer" onClick={() => openDoc(doc)}>
<CircleHelp className="inline-block w-4 h-4 cursor-pointer" />
</div>
)}
</label>
)}
<Select
value={selectedOption}
options={options}
@@ -200,13 +235,24 @@ export const Checkbox = (props: CheckboxProps) => {
interface FormGroupProps {
label?: string;
className?: string;
docKey?: string;
children: React.ReactNode;
}
export const FormGroup: React.FC<FormGroupProps> = ({ label, className, children }) => {
export const FormGroup: React.FC<FormGroupProps> = ({ label, className, children, docKey = null }) => {
const doc = getDoc(docKey);
return (
<div className={classNames(className)}>
{label && <label className={labelClasses}>{label}</label>}
{label && (
<label className={labelClasses}>
{label}{' '}
{doc && (
<div className="inline-block ml-1 text-xs text-gray-500 cursor-pointer" onClick={() => openDoc(doc)}>
<CircleHelp className="inline-block w-4 h-4 cursor-pointer" />
</div>
)}
</label>
)}
<div className="px-4 space-y-2">{children}</div>
</div>
);

60
ui/src/docs.tsx Normal file
View File

@@ -0,0 +1,60 @@
import React from 'react';
import { ConfigDoc } from '@/types';
const docs: { [key: string]: ConfigDoc } = {
'config.name': {
title: 'Training Name',
description: (
<>
The name of the training job. This name will be used to identify the job in the system and will the the filename
of the final model. It must be unique and can only contain alphanumeric characters, underscores, and dashes. No
spaces or special characters are allowed.
</>
),
},
'gpuids': {
title: 'GPU ID',
description: (
<>
This is the GPU that will be used for training. Only one GPU can be used per job at a time via the UI currently.
However, you can start multiple jobs in parallel, each using a different GPU.
</>
),
},
'config.process[0].trigger_word': {
title: 'Trigger Word',
description: (
<>
Optional: This will be the word or token used to trigger your concept or character.
<br />
<br />
When using a trigger word,
If your captions do not contain the trigger word, it will be added automatically the beginning of the caption. If you do not have
captions, the caption will become just the trigger word. If you want to have variable trigger words in your captions to put it in different spots,
you can use the <code>{'[trigger]'}</code> placeholder in your captions. This will be automatically replaced with your trigger word.
<br />
<br />
Trigger words will not automatically be added to your test prompts, so you will need to either add your trigger word manually or use the
<code>{'[trigger]'}</code> placeholder in your test prompts as well.
</>
),
},
'config.process[0].model.name_or_path': {
title: 'Name or Path',
description: (
<>
The name of a diffusers repo on Huggingface or the local path to the base model you want to train from. The folder needs to be in
diffusers format for most models. For some models, such as SDXL and SD1, you can put the path to an all in one safetensors checkpoint here.
</>
),
},
};
export const getDoc = (key: string | null | undefined): ConfigDoc | null => {
if (key && key in docs) {
return docs[key];
}
return null;
};
export default docs;

View File

@@ -59,7 +59,7 @@ export interface NetworkConfig {
lokr_factor: number;
network_kwargs: {
ignore_if_contains: string[];
}
};
}
export interface SaveConfig {
@@ -125,7 +125,7 @@ export interface ModelConfig {
quantize_kwargs?: QuantizeKwargsConfig;
arch: string;
low_vram: boolean;
model_kwargs: {[key: string]: any};
model_kwargs: { [key: string]: any };
}
export interface SampleConfig {
@@ -173,3 +173,8 @@ export interface JobConfig {
config: ConfigObject;
meta: MetaConfig;
}
export interface ConfigDoc {
title: string;
description: React.ReactNode;
}