[Docs] Rename docs_new/ to docs/ (#32123)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c949e91f18
commit
b819d2fb5b
@@ -0,0 +1,372 @@
|
||||
export const FluxDeployment = () => {
|
||||
const config = {
|
||||
modelFamily: 'FLUX',
|
||||
|
||||
options: {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'b300', label: 'B300', default: false },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi300x', label: 'MI300X', default: false },
|
||||
{ id: 'a2', label: 'A2', default: false },
|
||||
{ id: 'a3', label: 'A3', default: false }
|
||||
]
|
||||
},
|
||||
version: {
|
||||
name: 'version',
|
||||
title: 'Model Version',
|
||||
items: [
|
||||
{ id: 'flux1-dev', label: 'FLUX.1-dev', subtitle: '12B', default: true },
|
||||
{ id: 'flux2-dev', label: 'FLUX.2-dev', subtitle: '32B', default: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
modelConfigs: {
|
||||
'flux1-dev': { repoId: 'black-forest-labs/FLUX.1-dev' },
|
||||
'flux2-dev': { repoId: 'black-forest-labs/FLUX.2-dev' }
|
||||
},
|
||||
|
||||
generateCommand: function(values) {
|
||||
const { hardware, version } = values;
|
||||
const config = this.modelConfigs[version];
|
||||
|
||||
if (hardware === 'a2') {
|
||||
if (version === 'flux1-dev') {
|
||||
return `sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--num-gpus 1`;
|
||||
}
|
||||
|
||||
return `sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--tp-size 2 \\
|
||||
--num-gpus 2`;
|
||||
}
|
||||
|
||||
if (hardware === 'a3') {
|
||||
return `#One A3 card has 2 npu chips
|
||||
sglang serve \\
|
||||
--tp-size 2 \\
|
||||
--model-path ${config.repoId} \\
|
||||
--num-gpus 2`;
|
||||
}
|
||||
|
||||
return `sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--ulysses-degree=1 \\
|
||||
--ring-degree=1`;
|
||||
}
|
||||
};
|
||||
|
||||
if (!config || !config.options) {
|
||||
return <div>Error: Invalid configuration provided</div>;
|
||||
}
|
||||
|
||||
const getInitialState = () => {
|
||||
const initialState = {};
|
||||
Object.entries(config.options).forEach(([key, option]) => {
|
||||
if (option.type === 'checkbox') {
|
||||
initialState[key] = (option.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.type === 'text') {
|
||||
initialState[key] = option.default || '';
|
||||
return;
|
||||
}
|
||||
|
||||
let items = option.items || [];
|
||||
if (option.getDynamicItems) {
|
||||
const defaultValues = {};
|
||||
Object.entries(config.options).forEach(([innerKey, innerOption]) => {
|
||||
if (innerOption.type === 'checkbox') {
|
||||
defaultValues[innerKey] = (innerOption.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
} else if (innerOption.type === 'text') {
|
||||
defaultValues[innerKey] = innerOption.default || '';
|
||||
} else if (innerOption.items && innerOption.items.length > 0) {
|
||||
const defaultItem = innerOption.items.find((item) => item.default);
|
||||
defaultValues[innerKey] = defaultItem ? defaultItem.id : innerOption.items[0].id;
|
||||
}
|
||||
});
|
||||
items = option.getDynamicItems(defaultValues);
|
||||
}
|
||||
|
||||
const defaultItem = items && items.find((item) => item.default);
|
||||
initialState[key] = defaultItem ? defaultItem.id : items && items[0] ? items[0].id : '';
|
||||
});
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode =
|
||||
html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'style'],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isAscend = values.hardware === 'a2' || values.hardware === 'a3';
|
||||
const targetTabName = isAscend ? 'Ascend A3' : 'NVIDIA B200';
|
||||
|
||||
const allTabs = document.querySelectorAll('button, [role="tab"]');
|
||||
allTabs.forEach((tab) => {
|
||||
const text = tab.textContent.trim();
|
||||
if (text === targetTabName && tab.getAttribute('aria-selected') !== 'true') {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}, [values.hardware]);
|
||||
|
||||
const handleRadioChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (optionName, itemId, isChecked) => {
|
||||
setValues((prev) => {
|
||||
const currentValues = prev[optionName] || [];
|
||||
if (isChecked) {
|
||||
return { ...prev, [optionName]: [...currentValues, itemId] };
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[optionName]: currentValues.filter((id) => id !== itemId),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTextChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const command = config.generateCommand ? config.generateCommand.call(config, values) : '';
|
||||
|
||||
const containerStyle = {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
};
|
||||
const cardStyle = {
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: isDark ? '#1f2937' : '#fff',
|
||||
};
|
||||
const titleStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
minWidth: '140px',
|
||||
flexShrink: 0,
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const itemsStyle = {
|
||||
display: 'flex',
|
||||
rowGap: '2px',
|
||||
columnGap: '6px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
};
|
||||
const labelBaseStyle = {
|
||||
padding: '4px 10px',
|
||||
border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
userSelect: 'none',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center',
|
||||
flex: 1,
|
||||
background: isDark ? '#374151' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const checkedStyle = {
|
||||
background: '#D45D44',
|
||||
color: 'white',
|
||||
borderColor: '#D45D44',
|
||||
};
|
||||
const disabledStyle = {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
};
|
||||
const subtitleStyle = {
|
||||
display: 'block',
|
||||
fontSize: '9px',
|
||||
marginTop: '1px',
|
||||
lineHeight: '1.1',
|
||||
opacity: 0.7,
|
||||
};
|
||||
const textInputStyle = {
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`,
|
||||
background: isDark ? '#111827' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : '#111827',
|
||||
fontSize: '13px',
|
||||
};
|
||||
const commandDisplayStyle = {
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: isDark ? '#111827' : '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: isDark ? '#e5e7eb' : '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(config.options).map(([key, option]) => {
|
||||
if (option.condition && !option.condition(values)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = option.getDynamicItems ? option.getDynamicItems(values) : option.items || [];
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{option.type === 'text' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={values[option.name] || ''}
|
||||
placeholder={option.placeholder || ''}
|
||||
onChange={(event) => handleTextChange(option.name, event.target.value)}
|
||||
style={textInputStyle}
|
||||
/>
|
||||
) : option.type === 'checkbox' ? (
|
||||
(option.items || []).map((item) => {
|
||||
const isChecked = (values[option.name] || []).includes(item.id);
|
||||
const isDisabled =
|
||||
item.required ||
|
||||
(typeof item.disabledWhen === 'function' && item.disabledWhen(values));
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(event) =>
|
||||
handleCheckboxChange(option.name, item.id, event.target.checked)
|
||||
}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
const isDisabled = Boolean(item.disabled);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
value={item.id}
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={() => !isDisabled && handleRadioChange(option.name, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{command}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,244 @@
|
||||
export const LTXDeployment = () => {
|
||||
const options = {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Deployment Target',
|
||||
items: [
|
||||
{ id: 'h200', label: '1x H200', subtitle: 'resident', default: true },
|
||||
{ id: 'h200-2gpu', label: '2 GPUs', subtitle: 'CFG parallel', default: false },
|
||||
{ id: 'h200-4gpu', label: '4 GPUs', subtitle: 'TP2 + CFG', default: false },
|
||||
{ id: 'standard', label: 'Standard CUDA', subtitle: 'Original mode', default: false },
|
||||
{ id: 'official', label: 'Official Match', subtitle: 'Original switching', default: false },
|
||||
],
|
||||
},
|
||||
model: {
|
||||
name: 'model',
|
||||
title: 'Model',
|
||||
items: [
|
||||
{ id: 'ltx23', label: 'LTX-2.3', default: true },
|
||||
{ id: 'ltx2', label: 'LTX-2', default: false },
|
||||
],
|
||||
},
|
||||
pipeline: {
|
||||
name: 'pipeline',
|
||||
title: 'Pipeline',
|
||||
items: [
|
||||
{ id: 'two-stage', label: 'Two Stage', default: true, validModels: ['ltx2', 'ltx23'] },
|
||||
{ id: 'two-stage-hq', label: 'Two Stage HQ', subtitle: 'High Quality', default: false, validModels: ['ltx23'] },
|
||||
{ id: 'one-stage', label: 'One Stage', default: false, validModels: ['ltx2', 'ltx23'] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const modelConfigs = {
|
||||
ltx2: {
|
||||
repoId: 'Lightricks/LTX-2',
|
||||
pipelines: {
|
||||
'one-stage': 'LTX2Pipeline',
|
||||
'two-stage': 'LTX2TwoStagePipeline',
|
||||
},
|
||||
supportedLoras: [],
|
||||
},
|
||||
ltx23: {
|
||||
repoId: 'Lightricks/LTX-2.3',
|
||||
pipelines: {
|
||||
'one-stage': 'LTX2Pipeline',
|
||||
'two-stage': 'LTX2TwoStagePipeline',
|
||||
'two-stage-hq': 'LTX2TwoStageHQPipeline',
|
||||
},
|
||||
supportedLoras: [
|
||||
{
|
||||
id: 'transition',
|
||||
path: 'valiantcat/LTX-2.3-Transition-LORA',
|
||||
weightName: 'ltx2.3-transition.safetensors',
|
||||
validPipelines: ['two-stage', 'two-stage-hq'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const getInitialState = () => ({
|
||||
hardware: 'h200',
|
||||
model: 'ltx23',
|
||||
pipeline: 'two-stage',
|
||||
selectedLoraPath: 'none',
|
||||
});
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode = html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'style'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const availableLoras = (() => {
|
||||
const config = modelConfigs[values.model];
|
||||
return (config?.supportedLoras || []).filter((lora) => lora.validPipelines.includes(values.pipeline));
|
||||
})();
|
||||
|
||||
const handleRadioChange = (optionName, itemId) => {
|
||||
setValues((prev) => {
|
||||
const next = { ...prev, [optionName]: itemId };
|
||||
|
||||
const validPipeline = options.pipeline.items.some((item) => (
|
||||
item.id === next.pipeline && item.validModels.includes(next.model)
|
||||
));
|
||||
if (!validPipeline) {
|
||||
next.pipeline = 'two-stage';
|
||||
}
|
||||
|
||||
const config = modelConfigs[next.model];
|
||||
const nextSupported = (config?.supportedLoras || []).filter((lora) => lora.validPipelines.includes(next.pipeline));
|
||||
const isValid = nextSupported.some((lora) => lora.path === prev.selectedLoraPath);
|
||||
if (!isValid) {
|
||||
next.selectedLoraPath = 'none';
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoraToggle = (path) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
selectedLoraPath: prev.selectedLoraPath === path ? 'none' : path,
|
||||
}));
|
||||
};
|
||||
|
||||
const getDeviceMode = () => {
|
||||
if (values.hardware.startsWith('h200')) {
|
||||
return 'resident';
|
||||
}
|
||||
if (values.hardware === 'official') {
|
||||
return 'original';
|
||||
}
|
||||
return 'original';
|
||||
};
|
||||
|
||||
const getParallelFlags = () => {
|
||||
const parallelFlagsMap = {
|
||||
'h200-2gpu': ` \\\n --num-gpus 2 \\\n --enable-cfg-parallel`,
|
||||
'h200-4gpu': ` \\\n --num-gpus 4 \\\n --tp-size 2 \\\n --enable-cfg-parallel`,
|
||||
};
|
||||
return parallelFlagsMap[values.hardware] || '';
|
||||
};
|
||||
|
||||
const generateCommand = () => {
|
||||
const config = modelConfigs[values.model];
|
||||
const pipelineClass = config.pipelines[values.pipeline];
|
||||
if (!pipelineClass) {
|
||||
return '# Error: Invalid configuration';
|
||||
}
|
||||
|
||||
let command = `sglang serve \\\n --model-path ${config.repoId} \\\n --pipeline-class-name ${pipelineClass}`;
|
||||
command += getParallelFlags();
|
||||
if (values.model === 'ltx23' && values.pipeline !== 'one-stage') {
|
||||
command += ` \\\n --ltx2-two-stage-device-mode ${getDeviceMode()}`;
|
||||
}
|
||||
|
||||
const selectedLora = availableLoras.find((lora) => lora.path === values.selectedLoraPath);
|
||||
if (selectedLora) {
|
||||
command += ` \\\n --lora-path ${selectedLora.path} \\\n --lora-weight-name ${selectedLora.weightName}`;
|
||||
}
|
||||
|
||||
command += ` \\\n --port 30000`;
|
||||
return command;
|
||||
};
|
||||
|
||||
const containerStyle = { maxWidth: '900px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '4px' };
|
||||
const cardStyle = { padding: '8px 12px', border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`, borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`, borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '12px', background: isDark ? '#1f2937' : '#fff' };
|
||||
const titleStyle = { fontSize: '13px', fontWeight: '600', minWidth: '140px', flexShrink: 0, color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const itemsStyle = { display: 'flex', rowGap: '2px', columnGap: '6px', flexWrap: 'wrap', alignItems: 'center', flex: 1 };
|
||||
const labelBaseStyle = { padding: '4px 10px', border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`, borderRadius: '3px', cursor: 'pointer', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontWeight: '500', fontSize: '13px', transition: 'all 0.2s', userSelect: 'none', minWidth: '45px', textAlign: 'center', flex: 1, background: isDark ? '#374151' : '#fff', color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const checkedStyle = { background: '#D45D44', color: 'white', borderColor: '#D45D44' };
|
||||
const subtitleStyle = { display: 'block', fontSize: '9px', marginTop: '1px', lineHeight: '1.1', opacity: 0.7 };
|
||||
const commandDisplayStyle = { flex: 1, padding: '12px 16px', background: isDark ? '#111827' : '#f5f5f5', borderRadius: '6px', fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace", fontSize: '12px', lineHeight: '1.5', color: isDark ? '#e5e7eb' : '#374151', whiteSpace: 'pre-wrap', overflowX: 'auto', margin: 0, border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}` };
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(options).map(([key, option]) => {
|
||||
const itemsToDisplay = key === 'pipeline'
|
||||
? option.items.filter((item) => item.validModels.includes(values.model))
|
||||
: option.items;
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{itemsToDisplay.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
return (
|
||||
<label key={item.id} style={{ ...labelBaseStyle, ...(isChecked ? checkedStyle : {}) }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
checked={isChecked}
|
||||
onChange={() => handleRadioChange(key, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small style={{ ...subtitleStyle, color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Select LoRA Model</div>
|
||||
<div style={itemsStyle}>
|
||||
{availableLoras.length === 0 && (
|
||||
<div style={{ color: isDark ? '#999' : '#666', fontSize: '12px', padding: '8px' }}>
|
||||
No LoRA models available for this configuration.
|
||||
</div>
|
||||
)}
|
||||
{availableLoras.map((lora) => {
|
||||
const isSelected = values.selectedLoraPath === lora.path;
|
||||
return (
|
||||
<label
|
||||
key={lora.id}
|
||||
style={{ ...labelBaseStyle, ...(isSelected ? checkedStyle : {}) }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleLoraToggle(lora.path);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="loraModelSelection"
|
||||
checked={isSelected}
|
||||
readOnly
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{lora.id}
|
||||
<small style={{ ...subtitleStyle, color: isSelected ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{lora.path}
|
||||
</small>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{generateCommand()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const DiffusionModelTags = ({ tags = [] }) => {
|
||||
const normalizedTags = Array.isArray(tags) ? tags : [tags];
|
||||
|
||||
return (
|
||||
<div className="not-prose sgd-model-tags">
|
||||
{normalizedTags.map((tag) => (
|
||||
<span key={tag} className="sgd-chip">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
export const MOVADeployment = () => {
|
||||
// Config options
|
||||
const options = {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'a100', label: 'A100', default: false }
|
||||
]
|
||||
},
|
||||
resolution: {
|
||||
name: 'resolution',
|
||||
title: 'Resolution',
|
||||
items: [
|
||||
{ id: '360p', label: '360p', subtitle: 'Fast inference, lower VRAM', default: true },
|
||||
{ id: '720p', label: '720p', subtitle: 'Higher resolution', default: false }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize state
|
||||
const getInitialState = () => {
|
||||
const initialState = {};
|
||||
Object.entries(options).forEach(([key, option]) => {
|
||||
const defaultItem = option.items.find(item => item.default);
|
||||
initialState[key] = defaultItem ? defaultItem.id : option.items[0].id;
|
||||
});
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
// Detect dark mode
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode = html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'style'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleRadioChange = (optionName, value) => {
|
||||
setValues(prev => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
// Generate command
|
||||
const generateCommand = () => {
|
||||
const { resolution } = values;
|
||||
const modelPath = resolution === '720p'
|
||||
? 'OpenMOSS-Team/MOVA-720p'
|
||||
: 'OpenMOSS-Team/MOVA-360p';
|
||||
|
||||
return `export SG_OUTPUT_DIR=/root/output_mova
|
||||
mkdir -p "$SG_OUTPUT_DIR"
|
||||
|
||||
sglang serve \\
|
||||
--model-path ${modelPath} \\
|
||||
--host 0.0.0.0 \\
|
||||
--port 30002 \\
|
||||
--adjust-frames false \\
|
||||
--num-gpus 8 \\
|
||||
--ring-degree 2 \\
|
||||
--ulysses-degree 4 \\
|
||||
--tp 1 \\
|
||||
--enable-torch-compile \\
|
||||
--save-output \\
|
||||
--output-dir "$SG_OUTPUT_DIR"`;
|
||||
};
|
||||
|
||||
// Styles - with dark mode support
|
||||
const containerStyle = { maxWidth: '900px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '4px' };
|
||||
const cardStyle = { padding: '8px 12px', border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`, borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`, borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '12px', background: isDark ? '#1f2937' : '#fff' };
|
||||
const titleStyle = { fontSize: '13px', fontWeight: '600', minWidth: '140px', flexShrink: 0, color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const itemsStyle = { display: 'flex', rowGap: '2px', columnGap: '6px', flexWrap: 'wrap', alignItems: 'center', flex: 1 };
|
||||
const labelBaseStyle = { padding: '4px 10px', border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`, borderRadius: '3px', cursor: 'pointer', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontWeight: '500', fontSize: '13px', transition: 'all 0.2s', userSelect: 'none', minWidth: '45px', textAlign: 'center', flex: 1, background: isDark ? '#374151' : '#fff', color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const checkedStyle = { background: '#D45D44', color: 'white', borderColor: '#D45D44' };
|
||||
const subtitleStyle = { display: 'block', fontSize: '9px', marginTop: '1px', lineHeight: '1.1', opacity: 0.7 };
|
||||
const commandDisplayStyle = { flex: 1, padding: '12px 16px', background: isDark ? '#111827' : '#f5f5f5', borderRadius: '6px', fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace", fontSize: '12px', lineHeight: '1.5', color: isDark ? '#e5e7eb' : '#374151', whiteSpace: 'pre-wrap', overflowX: 'auto', margin: 0, border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}` };
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(options).map(([key, option]) => (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{option.items.map(item => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
return (
|
||||
<label key={item.id} style={{ ...labelBaseStyle, ...(isChecked ? checkedStyle : {}) }}>
|
||||
<input type="radio" name={option.name} value={item.id} checked={isChecked} onChange={() => handleRadioChange(option.name, item.id)} style={{ display: 'none' }} />
|
||||
{item.label}
|
||||
{item.subtitle && <small style={{ ...subtitleStyle, color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit' }}>{item.subtitle}</small>}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{generateCommand()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,385 @@
|
||||
export const QwenImageDeployment = () => {
|
||||
const config = {
|
||||
modelFamily: 'Qwen-Image',
|
||||
|
||||
options: {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'b300', label: 'B300', default: false },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'mi300x', label: 'MI300X', default: false },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false },
|
||||
{ id: 'a2', label: 'A2', default: false },
|
||||
{ id: 'a3', label: 'A3', default: false }
|
||||
]
|
||||
},
|
||||
precision: {
|
||||
name: 'precision',
|
||||
title: 'Precision',
|
||||
items: [
|
||||
{ id: 'bf16', label: 'BF16', default: true },
|
||||
{
|
||||
id: 'nvfp4',
|
||||
label: 'NVFP4',
|
||||
default: false,
|
||||
disabledWhen: (values) => !['b200', 'b300'].includes(values.hardware),
|
||||
disabledReason: 'ModelOpt NVFP4 requires Blackwell hardware such as B200 or B300'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generateCommand: function(values) {
|
||||
if (values.hardware === 'a2') {
|
||||
return `sglang serve \\
|
||||
--model-path Qwen/Qwen-Image \\
|
||||
--num-gpus 1`;
|
||||
}
|
||||
|
||||
if (values.hardware === 'a3') {
|
||||
return `#One A3 card has 2 npu chips
|
||||
sglang serve \\
|
||||
--model-path Qwen/Qwen-Image \\
|
||||
--tp-size 1 \\
|
||||
--sp-degree 2 \\
|
||||
--num-gpus 2`;
|
||||
}
|
||||
|
||||
const isBlackwell = ['b200', 'b300'].includes(values.hardware);
|
||||
const isNvfp4 = values.precision === 'nvfp4' && isBlackwell;
|
||||
const modelPath = isNvfp4
|
||||
? 'lmsys/qwen-image-2512-modelopt-nvfp4-sglang'
|
||||
: 'Qwen/Qwen-Image';
|
||||
|
||||
return `sglang serve \\
|
||||
--model-path ${modelPath} \\
|
||||
--ulysses-degree=1 \\
|
||||
--ring-degree=1`;
|
||||
}
|
||||
};
|
||||
|
||||
if (!config || !config.options) {
|
||||
return <div>Error: Invalid configuration provided</div>;
|
||||
}
|
||||
|
||||
const getInitialState = () => {
|
||||
const initialState = {};
|
||||
Object.entries(config.options).forEach(([key, option]) => {
|
||||
if (option.type === 'checkbox') {
|
||||
initialState[key] = (option.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.type === 'text') {
|
||||
initialState[key] = option.default || '';
|
||||
return;
|
||||
}
|
||||
|
||||
let items = option.items || [];
|
||||
if (option.getDynamicItems) {
|
||||
const defaultValues = {};
|
||||
Object.entries(config.options).forEach(([innerKey, innerOption]) => {
|
||||
if (innerOption.type === 'checkbox') {
|
||||
defaultValues[innerKey] = (innerOption.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
} else if (innerOption.type === 'text') {
|
||||
defaultValues[innerKey] = innerOption.default || '';
|
||||
} else if (innerOption.items && innerOption.items.length > 0) {
|
||||
const defaultItem = innerOption.items.find((item) => item.default);
|
||||
defaultValues[innerKey] = defaultItem ? defaultItem.id : innerOption.items[0].id;
|
||||
}
|
||||
});
|
||||
items = option.getDynamicItems(defaultValues);
|
||||
}
|
||||
|
||||
const defaultItem = items && items.find((item) => item.default);
|
||||
initialState[key] = defaultItem ? defaultItem.id : items && items[0] ? items[0].id : '';
|
||||
});
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode =
|
||||
html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'style'],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let targetTabName = 'AMD MI300X';
|
||||
if (values.hardware === 'a2') targetTabName = 'Ascend A2';
|
||||
if (values.hardware === 'a3') targetTabName = 'Ascend A3';
|
||||
|
||||
const allTabs = document.querySelectorAll('button, [role="tab"]');
|
||||
|
||||
allTabs.forEach((tab) => {
|
||||
const text = tab.textContent.trim();
|
||||
|
||||
if (text === targetTabName && tab.getAttribute('aria-selected') !== 'true') {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}, [values.hardware]);
|
||||
|
||||
const handleRadioChange = (optionName, value) => {
|
||||
setValues((prev) => {
|
||||
const next = { ...prev, [optionName]: value };
|
||||
if (
|
||||
optionName === 'hardware' &&
|
||||
!['b200', 'b300'].includes(value) &&
|
||||
next.precision === 'nvfp4'
|
||||
) {
|
||||
next.precision = 'bf16';
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (optionName, itemId, isChecked) => {
|
||||
setValues((prev) => {
|
||||
const currentValues = prev[optionName] || [];
|
||||
if (isChecked) {
|
||||
return { ...prev, [optionName]: [...currentValues, itemId] };
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[optionName]: currentValues.filter((id) => id !== itemId),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTextChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const command = config.generateCommand ? config.generateCommand.call(config, values) : '';
|
||||
|
||||
const containerStyle = {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
};
|
||||
const cardStyle = {
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: isDark ? '#1f2937' : '#fff',
|
||||
};
|
||||
const titleStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
minWidth: '140px',
|
||||
flexShrink: 0,
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const itemsStyle = {
|
||||
display: 'flex',
|
||||
rowGap: '2px',
|
||||
columnGap: '6px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
};
|
||||
const labelBaseStyle = {
|
||||
padding: '4px 10px',
|
||||
border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
userSelect: 'none',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center',
|
||||
flex: 1,
|
||||
background: isDark ? '#374151' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const checkedStyle = {
|
||||
background: '#D45D44',
|
||||
color: 'white',
|
||||
borderColor: '#D45D44',
|
||||
};
|
||||
const disabledStyle = {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
};
|
||||
const subtitleStyle = {
|
||||
display: 'block',
|
||||
fontSize: '9px',
|
||||
marginTop: '1px',
|
||||
lineHeight: '1.1',
|
||||
opacity: 0.7,
|
||||
};
|
||||
const textInputStyle = {
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`,
|
||||
background: isDark ? '#111827' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : '#111827',
|
||||
fontSize: '13px',
|
||||
};
|
||||
const commandDisplayStyle = {
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: isDark ? '#111827' : '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: isDark ? '#e5e7eb' : '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(config.options).map(([key, option]) => {
|
||||
if (option.condition && !option.condition(values)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = option.getDynamicItems ? option.getDynamicItems(values) : option.items || [];
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{option.type === 'text' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={values[option.name] || ''}
|
||||
placeholder={option.placeholder || ''}
|
||||
onChange={(event) => handleTextChange(option.name, event.target.value)}
|
||||
style={textInputStyle}
|
||||
/>
|
||||
) : option.type === 'checkbox' ? (
|
||||
(option.items || []).map((item) => {
|
||||
const isChecked = (values[option.name] || []).includes(item.id);
|
||||
const isDisabled =
|
||||
item.required ||
|
||||
(typeof item.disabledWhen === 'function' && item.disabledWhen(values));
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(event) =>
|
||||
handleCheckboxChange(option.name, item.id, event.target.checked)
|
||||
}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
const isDisabled =
|
||||
item.disabled ||
|
||||
(typeof item.disabledWhen === 'function' && item.disabledWhen(values));
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
value={item.id}
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={() => !isDisabled && handleRadioChange(option.name, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{command}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,320 @@
|
||||
export const QwenImageEditDeployment = () => {
|
||||
const config = {
|
||||
modelFamily: 'Qwen-Image-Edit',
|
||||
|
||||
options: {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'b300', label: 'B300', default: false },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'mi300x', label: 'MI300X', default: false },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generateCommand: function(values) {
|
||||
return `sglang serve \\
|
||||
--model-path Qwen/Qwen-Image-Edit-2511 \\
|
||||
--ulysses-degree=1 \\
|
||||
--ring-degree=1`;
|
||||
}
|
||||
};
|
||||
|
||||
if (!config || !config.options) {
|
||||
return <div>Error: Invalid configuration provided</div>;
|
||||
}
|
||||
|
||||
const getInitialState = () => {
|
||||
const initialState = {};
|
||||
Object.entries(config.options).forEach(([key, option]) => {
|
||||
if (option.type === 'checkbox') {
|
||||
initialState[key] = (option.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.type === 'text') {
|
||||
initialState[key] = option.default || '';
|
||||
return;
|
||||
}
|
||||
|
||||
let items = option.items || [];
|
||||
if (option.getDynamicItems) {
|
||||
const defaultValues = {};
|
||||
Object.entries(config.options).forEach(([innerKey, innerOption]) => {
|
||||
if (innerOption.type === 'checkbox') {
|
||||
defaultValues[innerKey] = (innerOption.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
} else if (innerOption.type === 'text') {
|
||||
defaultValues[innerKey] = innerOption.default || '';
|
||||
} else if (innerOption.items && innerOption.items.length > 0) {
|
||||
const defaultItem = innerOption.items.find((item) => item.default);
|
||||
defaultValues[innerKey] = defaultItem ? defaultItem.id : innerOption.items[0].id;
|
||||
}
|
||||
});
|
||||
items = option.getDynamicItems(defaultValues);
|
||||
}
|
||||
|
||||
const defaultItem = items && items.find((item) => item.default);
|
||||
initialState[key] = defaultItem ? defaultItem.id : items && items[0] ? items[0].id : '';
|
||||
});
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode =
|
||||
html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'style'],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleRadioChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (optionName, itemId, isChecked) => {
|
||||
setValues((prev) => {
|
||||
const currentValues = prev[optionName] || [];
|
||||
if (isChecked) {
|
||||
return { ...prev, [optionName]: [...currentValues, itemId] };
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[optionName]: currentValues.filter((id) => id !== itemId),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTextChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const command = config.generateCommand ? config.generateCommand.call(config, values) : '';
|
||||
|
||||
const containerStyle = {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
};
|
||||
const cardStyle = {
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: isDark ? '#1f2937' : '#fff',
|
||||
};
|
||||
const titleStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
minWidth: '140px',
|
||||
flexShrink: 0,
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const itemsStyle = {
|
||||
display: 'flex',
|
||||
rowGap: '2px',
|
||||
columnGap: '6px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
};
|
||||
const labelBaseStyle = {
|
||||
padding: '4px 10px',
|
||||
border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
userSelect: 'none',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center',
|
||||
flex: 1,
|
||||
background: isDark ? '#374151' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const checkedStyle = {
|
||||
background: '#D45D44',
|
||||
color: 'white',
|
||||
borderColor: '#D45D44',
|
||||
};
|
||||
const disabledStyle = {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
};
|
||||
const subtitleStyle = {
|
||||
display: 'block',
|
||||
fontSize: '9px',
|
||||
marginTop: '1px',
|
||||
lineHeight: '1.1',
|
||||
opacity: 0.7,
|
||||
};
|
||||
const textInputStyle = {
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`,
|
||||
background: isDark ? '#111827' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : '#111827',
|
||||
fontSize: '13px',
|
||||
};
|
||||
const commandDisplayStyle = {
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: isDark ? '#111827' : '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: isDark ? '#e5e7eb' : '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(config.options).map(([key, option]) => {
|
||||
if (option.condition && !option.condition(values)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = option.getDynamicItems ? option.getDynamicItems(values) : option.items || [];
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{option.type === 'text' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={values[option.name] || ''}
|
||||
placeholder={option.placeholder || ''}
|
||||
onChange={(event) => handleTextChange(option.name, event.target.value)}
|
||||
style={textInputStyle}
|
||||
/>
|
||||
) : option.type === 'checkbox' ? (
|
||||
(option.items || []).map((item) => {
|
||||
const isChecked = (values[option.name] || []).includes(item.id);
|
||||
const isDisabled =
|
||||
item.required ||
|
||||
(typeof item.disabledWhen === 'function' && item.disabledWhen(values));
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(event) =>
|
||||
handleCheckboxChange(option.name, item.id, event.target.checked)
|
||||
}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
const isDisabled = Boolean(item.disabled);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
value={item.id}
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={() => !isDisabled && handleRadioChange(option.name, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{command}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,400 @@
|
||||
export const Wan21Deployment = () => {
|
||||
const MODELSIZE_DEFS = [
|
||||
{
|
||||
id: '14b',
|
||||
label: '14B',
|
||||
subtitle: 'High-quality, 480P/720P',
|
||||
default: true,
|
||||
validTasks: ['t2v', 'i2v'],
|
||||
},
|
||||
{
|
||||
id: '1_3b',
|
||||
label: '1.3B',
|
||||
subtitle: 'Lightweight, 480P',
|
||||
default: false,
|
||||
validTasks: ['t2v'],
|
||||
},
|
||||
];
|
||||
|
||||
const modelConfigs = {
|
||||
't2v-14b': {
|
||||
repoId: 'Wan-AI/Wan2.1-T2V-14B-Diffusers',
|
||||
supportedLoras: [
|
||||
{ id: 'general', label: 'General Wan2.1 LoRA', path: 'NIVEDAN/wan2.1-lora' },
|
||||
],
|
||||
},
|
||||
't2v-1_3b': {
|
||||
repoId: 'Wan-AI/Wan2.1-T2V-1.3B-Diffusers',
|
||||
supportedLoras: [],
|
||||
},
|
||||
'i2v-14b': {
|
||||
repoId: 'Wan-AI/Wan2.1-I2V-14B-720P-Diffusers',
|
||||
supportedLoras: [
|
||||
{ id: 'fight', label: 'Fight Style LoRA', path: 'valiantcat/Wan2.1-Fight-LoRA' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'b300', label: 'B300', default: false },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'mi300x', label: 'MI300X', default: false },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false },
|
||||
{ id: 'a2', label: 'A2', default: false },
|
||||
{ id: 'a3', label: 'A3', default: false }
|
||||
],
|
||||
},
|
||||
task: {
|
||||
name: 'task',
|
||||
title: 'Task Type',
|
||||
items: [
|
||||
{ id: 't2v', label: 'Text-to-Video (T2V)', default: true },
|
||||
{ id: 'i2v', label: 'Image-to-Video (I2V)', default: false },
|
||||
],
|
||||
},
|
||||
modelsize: {
|
||||
name: 'modelsize',
|
||||
title: 'Model Variant',
|
||||
items: MODELSIZE_DEFS.map(({ validTasks, ...rest }) => rest),
|
||||
},
|
||||
bestPractice: {
|
||||
name: 'bestPractice',
|
||||
title: 'Optimization',
|
||||
items: [
|
||||
{ id: 'off', label: 'Standard', default: true },
|
||||
{ id: 'on', label: 'Best Practice', default: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function modelSizeItemsForTask(task) {
|
||||
return MODELSIZE_DEFS.filter((item) => item.validTasks.includes(task)).map(
|
||||
({ validTasks, ...rest }) => rest
|
||||
);
|
||||
}
|
||||
|
||||
const getInitialState = () => {
|
||||
const task = 't2v';
|
||||
const sizes = modelSizeItemsForTask(task);
|
||||
const modelsize = sizes.find((size) => size.default)?.id || sizes[0].id;
|
||||
const configKey = `${task}-${modelsize}`;
|
||||
const supported = modelConfigs[configKey]?.supportedLoras || [];
|
||||
return {
|
||||
hardware: 'b200',
|
||||
task,
|
||||
modelsize,
|
||||
bestPractice: 'off',
|
||||
selectedLoraPath: supported[0]?.path ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode =
|
||||
html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'style'],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isAscend = values.hardware === 'a2' || values.hardware === 'a3';
|
||||
|
||||
const targetTabName = isAscend ? 'Ascend A3' : 'NVIDIA B200';
|
||||
|
||||
const allTabs = document.querySelectorAll('button, [role="tab"]');
|
||||
|
||||
allTabs.forEach((tab) => {
|
||||
const text = tab.textContent.trim();
|
||||
|
||||
if (text === targetTabName && tab.getAttribute('aria-selected') !== 'true') {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}, [values.hardware]);
|
||||
|
||||
const handleRadioChange = (optionName, itemId) => {
|
||||
setValues((prev) => {
|
||||
let next = { ...prev, [optionName]: itemId };
|
||||
|
||||
if (optionName === 'task') {
|
||||
const sizes = modelSizeItemsForTask(itemId);
|
||||
if (!sizes.some((size) => size.id === next.modelsize)) {
|
||||
next.modelsize = sizes.find((size) => size.default)?.id || sizes[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (optionName === 'task' || optionName === 'modelsize') {
|
||||
const configKey = `${next.task}-${next.modelsize}`;
|
||||
const supported = modelConfigs[configKey]?.supportedLoras || [];
|
||||
if (supported.length === 0) {
|
||||
next.selectedLoraPath = '';
|
||||
} else if (
|
||||
next.selectedLoraPath &&
|
||||
!supported.some((lora) => lora.path === next.selectedLoraPath)
|
||||
) {
|
||||
next.selectedLoraPath = supported[0].path;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoraToggle = (path) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
selectedLoraPath: prev.selectedLoraPath === path ? '' : path,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTextChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const generateCommand = () => {
|
||||
const { hardware, task, modelsize, selectedLoraPath, bestPractice } = values;
|
||||
const configKey = `${task}-${modelsize}`;
|
||||
const config = modelConfigs[configKey];
|
||||
|
||||
if (!config) {
|
||||
return '# Error: Invalid configuration';
|
||||
}
|
||||
|
||||
if (hardware === 'a2' || hardware === 'a3') {
|
||||
const comment = hardware === 'a3'
|
||||
? '#One A3 card has 2 npu chips\n'
|
||||
: '';
|
||||
const isBestPractice = bestPractice === 'on';
|
||||
let command;
|
||||
|
||||
if (task === 't2v' && modelsize === '1_3b' && hardware === 'a2' && !isBestPractice) {
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--num-gpus 1`;
|
||||
} else {
|
||||
const tpSize = modelsize === '1_3b' ? (isBestPractice ? 4 : 1) : 2;
|
||||
const spDegree = modelsize === '14b' && isBestPractice ? 4 : 1;
|
||||
const numGpus = isBestPractice ? 8 : (hardware === 'a3' ? 2 : 4);
|
||||
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--tp-size ${tpSize} \\
|
||||
--sp-degree ${spDegree} \\
|
||||
--num-gpus ${numGpus}`;
|
||||
}
|
||||
|
||||
if (isBestPractice) {
|
||||
command += ` \\\n --attention-backend laser_attn`;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLoraPath === 'NIVEDAN/wan2.1-lora' ||
|
||||
selectedLoraPath === 'valiantcat/Wan2.1-Fight-LoRA'
|
||||
) {
|
||||
command += ` \\\n --lora-path ${selectedLoraPath}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
let command = `sglang serve \\\n --model-path ${config.repoId} \\\n --dit-layerwise-offload true`;
|
||||
|
||||
if (bestPractice === 'on') {
|
||||
command += ` \\\n --num-gpus 4 \\\n --ulysses-degree 2 \\\n --enable-cfg-parallel`;
|
||||
}
|
||||
|
||||
if (selectedLoraPath) {
|
||||
command += ` \\\n --lora-path ${selectedLoraPath}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
const modelSizeItems = modelSizeItemsForTask(values.task);
|
||||
const loraConfigKey = `${values.task}-${values.modelsize}`;
|
||||
const availableLoras = modelConfigs[loraConfigKey]?.supportedLoras || [];
|
||||
const command = generateCommand();
|
||||
|
||||
const containerStyle = {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
};
|
||||
const cardStyle = {
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: isDark ? '#1f2937' : '#fff',
|
||||
};
|
||||
const titleStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
minWidth: '140px',
|
||||
flexShrink: 0,
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const itemsStyle = {
|
||||
display: 'flex',
|
||||
rowGap: '2px',
|
||||
columnGap: '6px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
};
|
||||
const labelBaseStyle = {
|
||||
padding: '4px 10px',
|
||||
border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
userSelect: 'none',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center',
|
||||
flex: 1,
|
||||
background: isDark ? '#374151' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const checkedStyle = {
|
||||
background: '#D45D44',
|
||||
color: 'white',
|
||||
borderColor: '#D45D44',
|
||||
};
|
||||
const disabledStyle = {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
};
|
||||
const subtitleStyle = {
|
||||
display: 'block',
|
||||
fontSize: '9px',
|
||||
marginTop: '1px',
|
||||
lineHeight: '1.1',
|
||||
opacity: 0.7,
|
||||
};
|
||||
const textInputStyle = {
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`,
|
||||
background: isDark ? '#111827' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : '#111827',
|
||||
fontSize: '13px',
|
||||
};
|
||||
const commandDisplayStyle = {
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: isDark ? '#111827' : '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: isDark ? '#e5e7eb' : '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(options).map(([key, option]) => (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{(key === 'modelsize' ? modelSizeItems : option.items).map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
return (
|
||||
<label key={item.id} style={{ ...labelBaseStyle, ...(isChecked ? checkedStyle : {}) }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
value={item.id}
|
||||
checked={isChecked}
|
||||
onChange={() => handleRadioChange(option.name, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small style={{ ...subtitleStyle, color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{availableLoras.length > 0 && (
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Select LoRA Model (Only some of the supported LoRAs are listed here)</div>
|
||||
<div style={itemsStyle}>
|
||||
{availableLoras.map((lora) => {
|
||||
const isChecked = values.selectedLoraPath === lora.path;
|
||||
return (
|
||||
<label
|
||||
key={lora.id}
|
||||
style={{ ...labelBaseStyle, ...(isChecked ? checkedStyle : {}) }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleLoraToggle(lora.path);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="selectedLoraPath"
|
||||
value={lora.path}
|
||||
checked={isChecked}
|
||||
readOnly
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{lora.label}
|
||||
<small style={{ ...subtitleStyle, color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{lora.path}
|
||||
</small>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{command}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,289 @@
|
||||
|
||||
export const Wan22Deployment = () => {
|
||||
const options = {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'b300', label: 'B300', default: false },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'mi300x', label: 'MI300X', default: false },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false },
|
||||
{ id: 'a2', label: 'A2', default: false },
|
||||
{ id: 'a3', label: 'A3', default: false }
|
||||
],
|
||||
},
|
||||
task: {
|
||||
name: 'task',
|
||||
title: 'Task Type',
|
||||
items: [
|
||||
{ id: 'i2v', label: 'Image-to-Video (I2V)', default: false },
|
||||
{ id: 't2v', label: 'Text-to-Video (T2V)', default: true },
|
||||
{ id: 'ti2v', label: 'Text/Image-to-Video (TI2V)', default: false },
|
||||
],
|
||||
},
|
||||
modelsize: {
|
||||
name: 'modelsize',
|
||||
title: 'Model Size',
|
||||
items: [
|
||||
{ id: '14b', label: 'A14B', subtitle: 'Diffusers (A14B)', default: true, validTasks: ['i2v', 't2v'] },
|
||||
{ id: '5b', label: '5B', subtitle: 'Diffusers', default: false, validTasks: ['ti2v'] },
|
||||
],
|
||||
},
|
||||
bestPractice: {
|
||||
name: 'bestPractice',
|
||||
title: 'Optimization',
|
||||
items: [
|
||||
{ id: 'off', label: 'Standard', default: true },
|
||||
{ id: 'on', label: 'Best Practice', default: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const modelConfigs = {
|
||||
'i2v-14b': {
|
||||
repoId: 'Wan-AI/Wan2.2-I2V-A14B-Diffusers',
|
||||
supportedLoras: [{ id: 'distill', path: 'lightx2v/Wan2.2-Distill-Loras' }],
|
||||
},
|
||||
't2v-14b': {
|
||||
repoId: 'Wan-AI/Wan2.2-T2V-A14B-Diffusers',
|
||||
supportedLoras: [{ id: 'arcane', path: 'Cseti/wan2.2-14B-Arcane_Jinx-lora-v1' }],
|
||||
},
|
||||
'ti2v-5b': {
|
||||
repoId: 'Wan-AI/Wan2.2-TI2V-5B-Diffusers',
|
||||
supportedLoras: [],
|
||||
},
|
||||
};
|
||||
|
||||
const getInitialState = () => ({
|
||||
hardware: 'b200',
|
||||
task: 't2v',
|
||||
modelsize: '14b',
|
||||
bestPractice: 'off',
|
||||
selectedLoraPath: 'none',
|
||||
});
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode = html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'style'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const availableLoras = (() => {
|
||||
const configKey = `${values.task}-${values.modelsize}`;
|
||||
return modelConfigs[configKey]?.supportedLoras || [];
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
const isAscend = values.hardware === 'a2' || values.hardware === 'a3';
|
||||
|
||||
const targetTabName = isAscend ? 'Ascend A3' : 'NVIDIA B200';
|
||||
|
||||
const allTabs = document.querySelectorAll('button, [role="tab"]');
|
||||
|
||||
allTabs.forEach((tab) => {
|
||||
const text = tab.textContent.trim();
|
||||
|
||||
if (text === targetTabName && tab.getAttribute('aria-selected') !== 'true') {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}, [values.hardware]);
|
||||
|
||||
const handleRadioChange = (optionName, itemId) => {
|
||||
setValues((prev) => {
|
||||
const next = { ...prev, [optionName]: itemId };
|
||||
if (optionName === 'task') {
|
||||
next.modelsize = itemId === 'ti2v' ? '5b' : '14b';
|
||||
}
|
||||
|
||||
const configKey = `${next.task}-${next.modelsize}`;
|
||||
const nextSupported = modelConfigs[configKey]?.supportedLoras || [];
|
||||
const isValid = nextSupported.some((lora) => lora.path === prev.selectedLoraPath);
|
||||
if (!isValid) {
|
||||
next.selectedLoraPath = 'none';
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoraToggle = (path) => {
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
selectedLoraPath: prev.selectedLoraPath === path ? 'none' : path,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateCommand = () => {
|
||||
const { hardware, task, modelsize, selectedLoraPath, bestPractice } = values;
|
||||
const configKey = `${task}-${modelsize}`;
|
||||
const config = modelConfigs[configKey];
|
||||
if (!config) {
|
||||
return '# Error: Invalid configuration';
|
||||
}
|
||||
|
||||
|
||||
if (hardware === 'a2' || hardware === 'a3') {
|
||||
const comment = hardware === 'a3'
|
||||
? '#One A3 card has 2 npu chips\n'
|
||||
: '';
|
||||
const isBestPractice = bestPractice === 'on';
|
||||
let command;
|
||||
|
||||
if (task === 'ti2v') {
|
||||
if (isBestPractice) {
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--sp-degree 8 \\
|
||||
--num-gpus 8`;
|
||||
} else if (hardware === 'a2') {
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--num-gpus 1`;
|
||||
} else {
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--tp-size 1 \\
|
||||
--sp-degree 2 \\
|
||||
--num-gpus 2`;
|
||||
}
|
||||
} else {
|
||||
const spDegree = isBestPractice ? 4 : 1;
|
||||
const numGpus = isBestPractice ? 8 : (hardware === 'a3' ? 2 : 4);
|
||||
|
||||
command = `${comment}sglang serve \\
|
||||
--model-path ${config.repoId} \\
|
||||
--tp-size 2 \\
|
||||
--sp-degree ${spDegree} \\
|
||||
--num-gpus ${numGpus}`;
|
||||
}
|
||||
|
||||
if (isBestPractice) {
|
||||
command += ` \\\n --attention-backend laser_attn`;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLoraPath === 'lightx2v/Wan2.2-Distill-Loras' ||
|
||||
selectedLoraPath === 'Cseti/wan2.2-14B-Arcane_Jinx-lora-v1'
|
||||
) {
|
||||
command += ` \\\n --lora-path ${selectedLoraPath}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
let command = `sglang serve \\\n --model-path ${config.repoId} \\\n --dit-layerwise-offload true`;
|
||||
if (bestPractice === 'on') {
|
||||
if (hardware === 'b300') {
|
||||
command += ` \\\n --tp-size 2 \\\n --num-gpus 8 \\\n --sp-degree 2 \\\n --ulysses-degree 2 \\\n --enable-cfg-parallel`;
|
||||
} else {
|
||||
command += ` \\\n --num-gpus 4 \\\n --ulysses-degree 2 \\\n --enable-cfg-parallel`;
|
||||
}
|
||||
}
|
||||
if (selectedLoraPath && selectedLoraPath !== 'none') {
|
||||
command += ` \\\n --lora-path ${selectedLoraPath}`;
|
||||
}
|
||||
return command;
|
||||
};
|
||||
|
||||
const containerStyle = { maxWidth: '900px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '4px' };
|
||||
const cardStyle = { padding: '8px 12px', border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`, borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`, borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '12px', background: isDark ? '#1f2937' : '#fff' };
|
||||
const titleStyle = { fontSize: '13px', fontWeight: '600', minWidth: '140px', flexShrink: 0, color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const itemsStyle = { display: 'flex', rowGap: '2px', columnGap: '6px', flexWrap: 'wrap', alignItems: 'center', flex: 1 };
|
||||
const labelBaseStyle = { padding: '4px 10px', border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`, borderRadius: '3px', cursor: 'pointer', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontWeight: '500', fontSize: '13px', transition: 'all 0.2s', userSelect: 'none', minWidth: '45px', textAlign: 'center', flex: 1, background: isDark ? '#374151' : '#fff', color: isDark ? '#e5e7eb' : 'inherit' };
|
||||
const checkedStyle = { background: '#D45D44', color: 'white', borderColor: '#D45D44' };
|
||||
const subtitleStyle = { display: 'block', fontSize: '9px', marginTop: '1px', lineHeight: '1.1', opacity: 0.7 };
|
||||
const commandDisplayStyle = { flex: 1, padding: '12px 16px', background: isDark ? '#111827' : '#f5f5f5', borderRadius: '6px', fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace", fontSize: '12px', lineHeight: '1.5', color: isDark ? '#e5e7eb' : '#374151', whiteSpace: 'pre-wrap', overflowX: 'auto', margin: 0, border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}` };
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(options).map(([key, option]) => {
|
||||
const itemsToDisplay = key === 'modelsize'
|
||||
? option.items.filter((item) => item.validTasks.includes(values.task))
|
||||
: option.items;
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{itemsToDisplay.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
return (
|
||||
<label key={item.id} style={{ ...labelBaseStyle, ...(isChecked ? checkedStyle : {}) }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
checked={isChecked}
|
||||
onChange={() => handleRadioChange(key, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small style={{ ...subtitleStyle, color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Select LoRA Model (Only some of the supported LoRAs are listed here)</div>
|
||||
<div style={itemsStyle}>
|
||||
{availableLoras.length === 0 && (
|
||||
<div style={{ color: isDark ? '#999' : '#666', fontSize: '12px', padding: '8px' }}>
|
||||
No LoRA models available for this model.
|
||||
</div>
|
||||
)}
|
||||
{availableLoras.map((lora) => {
|
||||
const isSelected = values.selectedLoraPath === lora.path;
|
||||
return (
|
||||
<label
|
||||
key={lora.id}
|
||||
style={{ ...labelBaseStyle, ...(isSelected ? checkedStyle : {}) }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleLoraToggle(lora.path);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="loraModelSelection"
|
||||
checked={isSelected}
|
||||
readOnly
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{lora.id}
|
||||
<small style={{ ...subtitleStyle, color: isSelected ? 'rgba(255,255,255,0.85)' : 'inherit' }}>
|
||||
{lora.path}
|
||||
</small>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{generateCommand()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
export const ZImageTurboDeployment = () => {
|
||||
const config = {
|
||||
modelFamily: 'Z-Image-Turbo',
|
||||
|
||||
options: {
|
||||
hardware: {
|
||||
name: 'hardware',
|
||||
title: 'Hardware Platform',
|
||||
items: [
|
||||
{ id: 'mi300x', label: 'MI300X', default: true },
|
||||
{ id: 'mi325x', label: 'MI325X', default: false },
|
||||
{ id: 'mi355x', label: 'MI355X', default: false },
|
||||
{ id: 'b200', label: 'B200', default: true },
|
||||
{ id: 'h200', label: 'H200', default: false },
|
||||
{ id: 'h100', label: 'H100', default: false },
|
||||
{ id: 'a2', label: 'A2', default: false },
|
||||
{ id: 'a3', label: 'A3', default: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generateCommand: function(values) {
|
||||
const { hardware } = values;
|
||||
|
||||
if (hardware === 'a2') {
|
||||
return `sglang serve \\
|
||||
--model-path Tongyi-MAI/Z-Image-Turbo \\
|
||||
--num-gpus 1`;
|
||||
}
|
||||
|
||||
if (hardware === 'a3') {
|
||||
return `#One A3 card has 2 npu chips
|
||||
sglang serve \\
|
||||
--model-path Tongyi-MAI/Z-Image-Turbo \\
|
||||
--tp-size 2 \\
|
||||
--sp-degree 1 \\
|
||||
--num-gpus 2`;
|
||||
}
|
||||
|
||||
return `sglang serve \\
|
||||
--model-path Tongyi-MAI/Z-Image-Turbo \\
|
||||
--ulysses-degree=1 \\
|
||||
--ring-degree=1`;
|
||||
}
|
||||
};
|
||||
|
||||
if (!config || !config.options) {
|
||||
return <div>Error: Invalid configuration provided</div>;
|
||||
}
|
||||
|
||||
const getInitialState = () => {
|
||||
const initialState = {};
|
||||
Object.entries(config.options).forEach(([key, option]) => {
|
||||
if (option.type === 'checkbox') {
|
||||
initialState[key] = (option.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.type === 'text') {
|
||||
initialState[key] = option.default || '';
|
||||
return;
|
||||
}
|
||||
|
||||
let items = option.items || [];
|
||||
if (option.getDynamicItems) {
|
||||
const defaultValues = {};
|
||||
Object.entries(config.options).forEach(([innerKey, innerOption]) => {
|
||||
if (innerOption.type === 'checkbox') {
|
||||
defaultValues[innerKey] = (innerOption.items || [])
|
||||
.filter((item) => item.default)
|
||||
.map((item) => item.id);
|
||||
} else if (innerOption.type === 'text') {
|
||||
defaultValues[innerKey] = innerOption.default || '';
|
||||
} else if (innerOption.items && innerOption.items.length > 0) {
|
||||
const defaultItem = innerOption.items.find((item) => item.default);
|
||||
defaultValues[innerKey] = defaultItem ? defaultItem.id : innerOption.items[0].id;
|
||||
}
|
||||
});
|
||||
items = option.getDynamicItems(defaultValues);
|
||||
}
|
||||
|
||||
const defaultItem = items && items.find((item) => item.default);
|
||||
initialState[key] = defaultItem ? defaultItem.id : items && items[0] ? items[0].id : '';
|
||||
});
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const [values, setValues] = useState(getInitialState);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const html = document.documentElement;
|
||||
const isDarkMode =
|
||||
html.classList.contains('dark') ||
|
||||
html.getAttribute('data-theme') === 'dark' ||
|
||||
html.style.colorScheme === 'dark';
|
||||
setIsDark(isDarkMode);
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'style'],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isAscend = values.hardware === 'a2' || values.hardware === 'a3';
|
||||
const targetTabName = isAscend ? 'Ascend A2 / A3' : 'AMD MI300X';
|
||||
|
||||
const allTabs = document.querySelectorAll('button, [role="tab"]');
|
||||
allTabs.forEach((tab) => {
|
||||
const text = tab.textContent.trim();
|
||||
if (text === targetTabName && tab.getAttribute('aria-selected') !== 'true') {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}, [values.hardware]);
|
||||
|
||||
const handleRadioChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (optionName, itemId, isChecked) => {
|
||||
setValues((prev) => {
|
||||
const currentValues = prev[optionName] || [];
|
||||
if (isChecked) {
|
||||
return { ...prev, [optionName]: [...currentValues, itemId] };
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[optionName]: currentValues.filter((id) => id !== itemId),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleTextChange = (optionName, value) => {
|
||||
setValues((prev) => ({ ...prev, [optionName]: value }));
|
||||
};
|
||||
|
||||
const command = config.generateCommand ? config.generateCommand.call(config, values) : '';
|
||||
|
||||
const containerStyle = {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
};
|
||||
const cardStyle = {
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
borderLeft: `3px solid ${isDark ? '#E85D4D' : '#D45D44'}`,
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: isDark ? '#1f2937' : '#fff',
|
||||
};
|
||||
const titleStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
minWidth: '140px',
|
||||
flexShrink: 0,
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const itemsStyle = {
|
||||
display: 'flex',
|
||||
rowGap: '2px',
|
||||
columnGap: '6px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
};
|
||||
const labelBaseStyle = {
|
||||
padding: '4px 10px',
|
||||
border: `1px solid ${isDark ? '#9ca3af' : '#d1d5db'}`,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
userSelect: 'none',
|
||||
minWidth: '45px',
|
||||
textAlign: 'center',
|
||||
flex: 1,
|
||||
background: isDark ? '#374151' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : 'inherit',
|
||||
};
|
||||
const checkedStyle = {
|
||||
background: '#D45D44',
|
||||
color: 'white',
|
||||
borderColor: '#D45D44',
|
||||
};
|
||||
const disabledStyle = {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
};
|
||||
const subtitleStyle = {
|
||||
display: 'block',
|
||||
fontSize: '9px',
|
||||
marginTop: '1px',
|
||||
lineHeight: '1.1',
|
||||
opacity: 0.7,
|
||||
};
|
||||
const textInputStyle = {
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`,
|
||||
background: isDark ? '#111827' : '#fff',
|
||||
color: isDark ? '#e5e7eb' : '#111827',
|
||||
fontSize: '13px',
|
||||
};
|
||||
const commandDisplayStyle = {
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: isDark ? '#111827' : '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
color: isDark ? '#e5e7eb' : '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
border: `1px solid ${isDark ? '#374151' : '#e5e7eb'}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className="not-prose">
|
||||
{Object.entries(config.options).map(([key, option]) => {
|
||||
if (option.condition && !option.condition(values)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = option.getDynamicItems ? option.getDynamicItems(values) : option.items || [];
|
||||
|
||||
return (
|
||||
<div key={key} style={cardStyle}>
|
||||
<div style={titleStyle}>{option.title}</div>
|
||||
<div style={itemsStyle}>
|
||||
{option.type === 'text' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={values[option.name] || ''}
|
||||
placeholder={option.placeholder || ''}
|
||||
onChange={(event) => handleTextChange(option.name, event.target.value)}
|
||||
style={textInputStyle}
|
||||
/>
|
||||
) : option.type === 'checkbox' ? (
|
||||
(option.items || []).map((item) => {
|
||||
const isChecked = (values[option.name] || []).includes(item.id);
|
||||
const isDisabled =
|
||||
item.required ||
|
||||
(typeof item.disabledWhen === 'function' && item.disabledWhen(values));
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={(event) =>
|
||||
handleCheckboxChange(option.name, item.id, event.target.checked)
|
||||
}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const isChecked = values[option.name] === item.id;
|
||||
const isDisabled = Boolean(item.disabled);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
title={item.disabledReason || ''}
|
||||
style={{
|
||||
...labelBaseStyle,
|
||||
...(isChecked ? checkedStyle : {}),
|
||||
...(isDisabled ? disabledStyle : {}),
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={option.name}
|
||||
value={item.id}
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={() => !isDisabled && handleRadioChange(option.name, item.id)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{item.label}
|
||||
{item.subtitle && (
|
||||
<small
|
||||
style={{
|
||||
...subtitleStyle,
|
||||
color: isChecked ? 'rgba(255,255,255,0.85)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={cardStyle}>
|
||||
<div style={titleStyle}>Run this Command:</div>
|
||||
<pre style={commandDisplayStyle}>{command}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user