Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions studio/web/src/components/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,8 @@ function PathStringField({
return (
<FieldShell
{...shell}
// Paths are long: full width under the name, not squeezed into the side column.
stack={kind === 'path'}
below={
<>
{/* resume_state / resume_lora:贴字段的 dropdown,按 version 分组列文件 */}
Expand Down Expand Up @@ -669,31 +671,44 @@ function PathStringField({
</>
}
>
<input
type="text"
value={text}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
aria-label={label}
title={text || undefined}
className={'ds-inp' + (kind === 'path' ? ' ds-mono' : '')}
/>
{(kind === 'path' || suffix) && (
<span style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
{kind === 'path' && (
<button
ref={browseBtnRef}
type="button"
onClick={() => setPicking((p) => !p)}
disabled={disabled}
className="ds-ctl ds-ghost"
style={{ height: 24 }}
>
{useResumePicker ? t('field.browseProject') : t('field.browse')}
</button>
)}
{suffix}
{kind === 'path' ? (
// input + folder button on one row, as in the mockup
<span style={{ display: 'flex', gap: 6, width: '100%' }}>
<input
type="text"
value={text}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
aria-label={label}
title={text || undefined}
className="ds-inp ds-mono"
style={{ flex: 1 }}
/>
<button
ref={browseBtnRef}
type="button"
onClick={() => setPicking((p) => !p)}
disabled={disabled}
className="ds-iconbtn"
aria-label={useResumePicker ? t('field.browseProject') : t('field.browse')}
title={useResumePicker ? t('field.browseProject') : t('field.browse')}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></svg>
</button>
</span>
) : (
<input
type="text"
value={text}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
aria-label={label}
title={text || undefined}
className="ds-inp"
/>
)}
{suffix && (
<span style={{ display: 'flex', gap: 6, alignItems: 'center' }}>{suffix}</span>
)}
</FieldShell>
)
Expand Down
6 changes: 5 additions & 1 deletion studio/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,11 @@ function ProjectStepperNav({ pid, activeVid, currentStep, version, collapsed, in

const overviewActive = inRoute && currentStep === null
// ADR-0007 §11.5 cursor 派生:cursor 之前的 phase = done。
const cursorPhase: VersionPhase = (version?.phase as VersionPhase | undefined) ?? 'curating'
// The cursor only means something while the version is being prepared; a
// trained / training version has every step behind it (same as Overview).
const cursorPhase: VersionPhase = !version
? 'curating'
: version.status === 'preparing' ? version.phase : 'ready'
const cursorIdx = PHASE_ORDER.indexOf(cursorPhase)

const isStepDone = (key: string): boolean => {
Expand Down
2 changes: 1 addition & 1 deletion studio/web/src/components/StepShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import TaskLogDrawer, { type LogSource } from './TaskLogDrawer'
interface Props {
idx: number | string
title: string
subtitle?: string
subtitle?: ReactNode
/** Caption above the title; defaults to "STEP N" for a numeric idx. */
eyebrow?: string
/** Green eyebrow (the mockup's version steps). On unless set to false. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function PreprocessHeadTools({ projectId, versionId }: { projectId: numbe
>
{t('ppFrame.restoreAll')}{s.processed > 0 ? ` · ${s.processed}` : ''}
</button>
<Link className="ds-btn-primary" to={`/projects/${projectId}/v/${versionId}/edit`}>{t('ppFrame.next')}</Link>
<Link className="ds-btn-primary" to={`/projects/${projectId}/v/${versionId}/edit`}>{t('ppFrame.next')}<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.3" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h13m-5-6 6 6-6 6" /></svg></Link>
</>
)
}
14 changes: 7 additions & 7 deletions studio/web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@
"downloadFile": "Download",
"folderOpened": "Opened {{path}}",
"sourceLink": "Project #{{projectId}} / v#{{versionId}}",
"eyebrow": "Task · {{kind}}",
"eyebrow": "TASK · {{kind}}",
"startedAtShort": "started {{time}}",
"runningFor": "running {{time}}",
"queuedAtShort": "queued since {{time}}",
Expand Down Expand Up @@ -1927,7 +1927,7 @@
"deltaHint": "Net gain over the plain base model baseline",
"deltaHintShort": "Δ against the plain base model",
"waitingMetrics": "waiting for metrics",
"eyebrow": "Tools",
"eyebrow": "TOOLS",
"title": "Monitor",
"subtitle": "Checkpoint metrics against the validation set. Δ is the gain over the plain base model.",
"pickCkptsBtn": "Pick checkpoints",
Expand Down Expand Up @@ -2168,7 +2168,7 @@
"sweep": "Sweep:",
"nCkpts": "{{count}} checkpoints",
"versionTraining": "{{label}} (training)",
"eyebrow": "Tools",
"eyebrow": "TOOLS",
"guidance": "Guidance",
"randomSeed": "Random",
"sizeNote": "multiple of 64 · {{mp}} MP",
Expand Down Expand Up @@ -2773,7 +2773,7 @@
},
"reg": {
"title": "Reg set",
"subtitle": "Booru scrape or a neutral base-model prior. Mirrors the train structure or one 1_data/ folder."
"subtitle": "Booru scrape or a neutral base-model prior. Mirrors the train structure or one <code>1_data/</code> folder."
},
"train": {
"title": "Train",
Expand Down Expand Up @@ -3036,7 +3036,7 @@
"colRes": "Res",
"selectHint": "Select a preset to see its summary.",
"colName": "Name",
"eyebrow": "Tools",
"eyebrow": "TOOLS",
"title": "Presets",
"subtitle": "Global config templates. A project version takes a copy — edits there never change the preset.",
"headMenu": "More: import, export YAML",
Expand Down Expand Up @@ -3073,7 +3073,7 @@
"noPresetError": "Select a preset first",
"enqueuedNav": "Queued #{{id}}, check progress in /queue",
"scheduledNav": "Scheduled #{{id}} to start at {{time}}",
"startTrainBtn": "Start Training",
"startTrainBtn": "Start training",
"scheduleBtn": "Schedule training",
"scheduleHint": "Schedule training (delay by N hours or pick a time)",
"scheduleDelaySection": "Start after",
Expand Down Expand Up @@ -3133,7 +3133,7 @@
"pickerTitleConfigured": "Current version has its own config; click to pick a preset as a starting point to reset",
"pickerTitleEmpty": "Click to pick a preset as a starting point and create the current version's config",
"configChip": "config",
"scopedConfigLabel": "{{title}} / {{label}} config",
"scopedConfigLabel": "{{title}} · {{label}}",
"notConfiguredLabel": "Not configured — pick a preset to start",
"readPresetParams": "Read params from this preset",
"estDuration": "Est. duration",
Expand Down
14 changes: 7 additions & 7 deletions studio/web/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
},
"reg": {
"title": "Reg-набор",
"subtitle": "Сбор с booru или нейтральный приор базовой модели. Повторяет структуру train либо одна папка 1_data/."
"subtitle": "Сбор с booru или нейтральный приор базовой модели. Повторяет структуру train либо одна папка <code>1_data/</code>."
},
"train": {
"title": "Тренировка",
Expand Down Expand Up @@ -320,7 +320,7 @@
"deltaHint": "Чистый прирост относительно чистой базовой модели",
"deltaHintShort": "Δ относительно чистой базовой модели",
"waitingMetrics": "жду метрики",
"eyebrow": "Инструменты",
"eyebrow": "ИНСТРУМЕНТЫ",
"title": "Монитор",
"subtitle": "Метрики чекпоинтов против validation-набора. Δ — прирост к чистой базовой модели.",
"pickCkptsBtn": "Выбрать чекпоинты",
Expand Down Expand Up @@ -446,7 +446,7 @@
"noPresetError": "Сначала выберите пресет",
"enqueuedNav": "Поставлено в очередь #{{id}}, прогресс в /queue",
"scheduledNav": "Запланировано #{{id}} на {{time}}",
"startTrainBtn": "Запустить тренировку",
"startTrainBtn": "Начать тренировку",
"scheduleBtn": "Запланировать",
"scheduleHint": "Запланировать тренировку (через N часов или на конкретное время)",
"scheduleDelaySection": "Начать через",
Expand Down Expand Up @@ -506,7 +506,7 @@
"pickerTitleConfigured": "У версии свой конфиг; нажмите, чтобы выбрать пресет как отправную точку для сброса",
"pickerTitleEmpty": "Нажмите, чтобы выбрать пресет как отправную точку и создать конфиг версии",
"configChip": "конфиг",
"scopedConfigLabel": "Конфиг {{title}} / {{label}}",
"scopedConfigLabel": "{{title}} · {{label}}",
"notConfiguredLabel": "Не настроено — выберите пресет",
"readPresetParams": "Прочитать параметры из этого пресета",
"estDuration": "Примерно займёт",
Expand Down Expand Up @@ -1624,7 +1624,7 @@
"addYAxis": "+ Добавить ось Y",
"addLora": "+ Добавить LoRA",
"versionTraining": "{{label}} (тренируется)",
"eyebrow": "Инструменты",
"eyebrow": "ИНСТРУМЕНТЫ",
"singleMode": "Одиночно",
"xyMode": "XY-перебор",
"xyAxis": "Ось {{label}}",
Expand Down Expand Up @@ -2838,7 +2838,7 @@
"copyFailed": "Не удалось скопировать",
"defaultedFieldsBody": "Поля с недопустимыми значениями сброшены к значениям по умолчанию:",
"droppedFieldsBody": "Поля, которых нет в этой версии, пропущены:",
"eyebrow": "Инструменты",
"eyebrow": "ИНСТРУМЕНТЫ",
"title": "Пресеты",
"subtitle": "Глобальные шаблоны конфига. Версия проекта берёт копию — правки в ней не меняют пресет.",
"headMenu": "Ещё: импорт, экспорт YAML",
Expand Down Expand Up @@ -2984,7 +2984,7 @@
"downloadFile": "Скачать",
"folderOpened": "Открыто: {{path}}",
"sourceLink": "Проект #{{projectId}} / v#{{versionId}}",
"eyebrow": "Задача · {{kind}}",
"eyebrow": "ЗАДАЧА · {{kind}}",
"startedAtShort": "запущена {{time}}",
"runningFor": "идёт {{time}}",
"queuedAtShort": "в очереди с {{time}}",
Expand Down
4 changes: 2 additions & 2 deletions studio/web/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2259,7 +2259,7 @@
},
"reg": {
"title": "正则集(可选)",
"subtitle": "Booru 拉图或 base 模型自生成中性 prior;可镜像 train 结构或平铺单桶 1_data/"
"subtitle": "Booru 拉图或 base 模型自生成中性 prior;可镜像 train 结构或平铺单桶 <code>1_data/</code>"
},
"train": {
"title": "训练",
Expand Down Expand Up @@ -2577,7 +2577,7 @@
"pickerTitleConfigured": "当前 version 私有配置;点击展开选预设作为起点重置",
"pickerTitleEmpty": "点击选预设作为起点创建当前 version 配置",
"configChip": "配置",
"scopedConfigLabel": "{{title}} / {{label}} 配置",
"scopedConfigLabel": "{{title}} · {{label}}",
"notConfiguredLabel": "未配置 — 选预设作为起点",
"readPresetParams": "读取此预设参数",
"estDuration": "预计耗时",
Expand Down
2 changes: 1 addition & 1 deletion studio/web/src/pages/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ function NewProjectDialog({
/>
</FieldLabel>

<FieldLabel label={t('common.notes')} hint="note (optional)">
<FieldLabel label={t('common.notes')} hint="note">
<textarea
className="input"
value={form.note}
Expand Down
2 changes: 1 addition & 1 deletion studio/web/src/pages/project/steps/Curation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ export default function CurationPage() {
<button type="button" className={`ds-seg-item${!isVal ? ' ds-is-active' : ''}`} aria-pressed={!isVal} onClick={() => switchBucket('train')}>{t('curate.bucketTrain')}</button>
<button type="button" className={`ds-seg-item${isVal ? ' ds-is-active' : ''}`} aria-pressed={isVal} onClick={() => switchBucket('validation')}>{t('curate.bucketValShort')}</button>
</div>
<Link className="ds-btn-primary" to={`/projects/${project.id}/v/${activeVersion.id}/preprocess`}>{t('curate.next')}</Link>
<Link className="ds-btn-primary" to={`/projects/${project.id}/v/${activeVersion.id}/preprocess`}>{t('curate.next')}<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.3" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h13m-5-6 6 6-6 6" /></svg></Link>
</>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion studio/web/src/pages/project/steps/Download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export default function DownloadPage() {
<button type="button" className="ds-ctl" onClick={() => setShowPathPicker(true)} disabled={uploader.busy}>
{t('download.uploadFromPath')}
</button>
<Link className="ds-btn-primary" to={nextTo}>{t('dataset.nextCurate')}</Link>
<Link className="ds-btn-primary" to={nextTo}>{t('dataset.nextCurate')}<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.3" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h13m-5-6 6 6-6 6" /></svg></Link>
</>
}
/>
Expand Down
4 changes: 2 additions & 2 deletions studio/web/src/pages/project/steps/Regularization.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TFunction } from 'i18next'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Link, useOutletContext } from 'react-router-dom'
import {
api,
Expand Down Expand Up @@ -532,7 +532,7 @@ export default function RegularizationPage() {
idx={5}
eyebrow={t('reg.eyebrow')}
title={t('steps.reg.title')}
subtitle={t('steps.reg.subtitle')}
subtitle={<Trans i18nKey="steps.reg.subtitle" components={{ code: <code /> }} />}
actions={
<>
{!reg?.exists && <Link className="ds-ctl" to={trainUrl}>{t('reg.skipStep')}</Link>}
Expand Down
6 changes: 5 additions & 1 deletion studio/web/src/pages/project/steps/Train.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ export default function TrainPage() {
return base.replace(/\.(safetensors|ckpt|pt|bin|gguf)$/i, '') || String(config?.model_family ?? '—')
})()
const subtitle = config
? [modelName, `${String(config.lora_type ?? 'lora')} r${String(config.lora_rank ?? '—')}`, t('train.nParams', { count: totalFields })].join(' · ')
? <><code>{modelName}</code>{' · '}{[`${LORA_TYPE_LABEL[String(config.lora_type ?? 'lora')] ?? String(config.lora_type)} r${String(config.lora_rank ?? '—')}`, t('train.nParams', { count: totalFields })].join(' · ')}</>
: t('steps.train.subtitle')

const triggerUnderDop = !!config?.dop_enabled && groupFields('loss').includes('dop_enabled')
Expand Down Expand Up @@ -695,6 +695,7 @@ export default function TrainPage() {
disabled={busy || !configResp?.has_config}
className="ds-btn-primary"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z" /></svg>
{t('train.startTrainBtn')}
</button>
{scheduleOpen && (
Expand Down Expand Up @@ -967,6 +968,9 @@ type TrainTabId = 'model' | 'data' | 'optim' | 'system'
type FieldFilterMode = 'all' | 'changed' | 'nondefault' | 'locked'

/** Which schema groups each tab of the form shows. */
/** How the network type reads in the page subtitle (the mockup writes "LoKr r32"). */
const LORA_TYPE_LABEL: Record<string, string> = { lora: 'LoRA', lokr: 'LoKr', loha: 'LoHa', ortho: 'OrthoLoRA', tlora: 'T-LoRA' }

const TRAIN_TABS = [
{ id: 'model', groups: ['model', 'lora'] },
{ id: 'data', groups: ['dataset', 'caption'] },
Expand Down
20 changes: 10 additions & 10 deletions studio/web/src/pages/tools/Presets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default function PresetsPage() {
// ── backend state ──
const [schema, setSchema] = useState<SchemaResponse | null>(null)
const [presets, setPresets] = useState<PresetSummary[]>([])
const [presetsLoaded, setPresetsLoaded] = useState(false)
const [selected, setSelected] = useState<string | null>(null)
const [config, setConfig] = useState<ConfigData | null>(null)
const [busy, setBusy] = useState(false)
Expand Down Expand Up @@ -156,13 +157,14 @@ export default function PresetsPage() {
const refreshList = () => {
api.listPresets().then((list) => {
setPresets(list)
setPresetsLoaded(true)
// 拉每个 preset 的 config 填表格列(best-effort,失败列显示 —)。
list.forEach((p) => {
api.getPreset(p.name)
.then((c) => setConfigCache((m) => ({ ...m, [p.name]: c })))
.catch(() => {})
})
}).catch(() => setPresets([]))
}).catch(() => { setPresets([]); setPresetsLoaded(true) })
}

// ── 选 preset 切换 ──
Expand Down Expand Up @@ -228,14 +230,12 @@ export default function PresetsPage() {
// ── 首次拿到列表后:自动选最近一个,省一次「切换」点击 ──
const autoSelectedRef = useRef(false)
useEffect(() => {
if (autoSelectedRef.current) return
if (presets.length > 0 && selected === null) {
autoSelectedRef.current = true
setSelected(presets[0].name)
} else if (presets.length === 0 && schema) {
autoSelectedRef.current = true
}
}, [presets, selected, schema])
// Wait for the list itself: the schema can arrive first, and an empty
// list at that moment is "not loaded yet", not "no presets".
if (autoSelectedRef.current || !presetsLoaded) return
autoSelectedRef.current = true
if (presets.length > 0 && selected === null) setSelected(presets[0].name)
}, [presets, presetsLoaded, selected])

// 编辑器开着时 Esc 关闭(dirty 时仍可关 —— 改动留在内存,跟切 preset 一致)。
useEffect(() => {
Expand Down Expand Up @@ -632,7 +632,7 @@ export default function PresetsPage() {
</td>
<td className="ds-num">{cfgRank(c)}</td>
<td className="ds-num">{cfgRes(c)}</td>
<td>{c?.optimizer_type ? schemaEnumLabel('optimizer_type', String(c.optimizer_type), t) : '—'}</td>
<td style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 130 }} title={c?.optimizer_type ? schemaEnumLabel('optimizer_type', String(c.optimizer_type), t) : undefined}>{c?.optimizer_type ? schemaEnumLabel('optimizer_type', String(c.optimizer_type), t) : '—'}</td>
<td className="ds-muted" style={{ whiteSpace: 'nowrap' }}>{fmtUpdated(p.updated_at)}</td>
<td onClick={(e) => e.stopPropagation()}>
<KebabMenu label={t('presets.rowMenu', { name: p.name })} items={rowMenu(p.name)} />
Expand Down
Loading
Loading