Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd29235
design: 2026 design system and app shell from the approved mockup
DualChimerra Sep 24, 2026
fc878ae
test(web): SystemStats readout follows the mockup (GPU · VRAM · CPU ·…
DualChimerra Sep 24, 2026
46d225a
design(projects): Projects page as in the mockup
DualChimerra Sep 24, 2026
4b24416
design(overview): project overview as in the mockup
DualChimerra Sep 24, 2026
189fde0
design(dataset): dataset page as in the mockup
DualChimerra Sep 24, 2026
7579382
design(curate): curate step as in the mockup
DualChimerra Sep 24, 2026
050ed55
design(preprocess): preprocess tools as in the mockup
DualChimerra Sep 24, 2026
6b47e9f
design(tag-edit): tag editing step as in the mockup
DualChimerra Sep 24, 2026
656d030
design(reg): reg set step as in the mockup
DualChimerra Sep 24, 2026
af3d4a9
design(train): training step as in the mockup
DualChimerra Sep 24, 2026
784a8dc
design(fields): setting descriptions open on hover over the name
DualChimerra Sep 24, 2026
d5e97c7
design(queue): queue page as in the mockup
DualChimerra Sep 25, 2026
7f4ae0a
design(queue-detail): task page as in the mockup
DualChimerra Sep 25, 2026
ef38a79
design(generate): generate page as in the mockup
DualChimerra Sep 25, 2026
48cd054
test(generate): find the XY axis card by its new frame
DualChimerra Sep 25, 2026
9646917
design(presets): presets page as in the mockup
DualChimerra Sep 25, 2026
0ac72f3
design(monitor): monitor page as in the mockup
DualChimerra Sep 25, 2026
8ea4037
design(soup): soup page as in the mockup
DualChimerra Sep 25, 2026
e813d43
design(search): Ctrl+K palette as in the mockup
DualChimerra Sep 25, 2026
2efb809
design(settings): settings drawer as in the mockup
DualChimerra Sep 25, 2026
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
4 changes: 3 additions & 1 deletion studio/api/routers/projects/curation.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ def list_files(pid: int, bucket: str = "download") -> dict[str, Any]:
if pdir.exists():
for f in sorted(pdir.iterdir()):
if f.is_file() and f.suffix.lower() in datasets.IMAGE_EXTS:
st = f.stat()
items.append({
"name": f.name,
"size": f.stat().st_size,
"size": st.st_size,
"mtime": st.st_mtime,
"has_meta": f.with_suffix(".booru.txt").exists(),
})
return {"items": items, "count": len(items)}
Expand Down
28 changes: 15 additions & 13 deletions studio/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,22 @@ function RootLayout() {
}, [navOpen])

return (
<div className={`flex h-[100dvh] overflow-hidden bg-sunken`}>
<Sidebar mobile={isMobile} mobileOpen={navOpen} onMobileClose={() => setNavOpen(false)} />
{isMobile && navOpen && (
<div
className="fixed inset-0 z-40 bg-zinc-950/40 backdrop-blur-[2px]"
onClick={() => setNavOpen(false)}
aria-hidden="true"
/>
)}
{/* Tailark 式 inset 面板:内容区作为一张浮在侧栏底色上的卡片(手机端全屏无边距)。 */}
<div className={`flex-1 min-w-0 flex ${isMobile ? '' : 'py-2 pr-2'}`}>
<div className={`flex-1 min-w-0 flex flex-col relative overflow-hidden bg-canvas ${isMobile ? '' : 'rounded-xl border border-subtle shadow-sm'}`}>
// The whole app is one white panel on the warm canvas (mockup .viewport /
// .app): sidebar and main column share it. On a phone the panel is
// full-bleed and the sidebar slides in as a drawer.
<div className="ds-viewport">
<div className="ds-app">
<Sidebar mobile={isMobile} mobileOpen={navOpen} onMobileClose={() => setNavOpen(false)} />
{isMobile && navOpen && (
<div
className="fixed inset-0 z-40 bg-zinc-950/40 backdrop-blur-[2px]"
onClick={() => setNavOpen(false)}
aria-hidden="true"
/>
)}
<div className="ds-main relative overflow-hidden">
<Topbar mobile={isMobile} onOpenNav={() => setNavOpen(true)} />
<main style={{ flex: 1, overflow: 'auto', background: 'var(--bg-canvas)' }}>
<main style={{ flex: 1, overflow: 'auto' }}>
<Outlet />
</main>
<SettingsDrawer />
Expand Down
2 changes: 2 additions & 0 deletions studio/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,8 @@ export interface Job {
export interface DownloadFile {
name: string
size: number
/** File mtime (unix seconds); absent from servers older than the 2026 redesign. */
mtime?: number
has_meta: boolean
}

Expand Down
63 changes: 58 additions & 5 deletions studio/web/src/components/BulkActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ interface Props {
tagSuggestions?: string[]
/** 用来给"未选时"的 hint 显示总数。 */
totalCount: number
/** 'bar': one-line green strip under the image grid (mockup TagEdit);
* remove / replace then live in the tag statistics card. */
variant?: 'panel' | 'bar'
}

/** 批量操作面板 — V2「行式」布局。
Expand All @@ -38,6 +41,7 @@ export default function BulkActionBar({
onClearSelection,
tagSuggestions = [],
totalCount,
variant = 'panel',
}: Props) {
const { t } = useTranslation()
const { toast } = useToast()
Expand Down Expand Up @@ -146,6 +150,51 @@ export default function BulkActionBar({
const noneSelected = selectedKeys.length === 0
const opDisabled = noneSelected

if (variant === 'bar') {
return (
<div className="ds-bulkbar" style={{ flexWrap: 'wrap', rowGap: 6 }}>
<span className="ds-cbox ds-on" style={{ borderColor: 'var(--green-600)', background: 'var(--green-600)' }} aria-hidden="true">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.2" strokeLinecap="round"><path d="m5 13 4 4L19 7" /></svg>
</span>
<span style={{ fontWeight: 500, whiteSpace: 'nowrap' }}>{t('bulkAction.selectedTotal', { n: selectedKeys.length, total: totalCount })}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, flex: '1 1 260px', minWidth: 0 }}>
<TagsField
value={addInput}
onChange={setAddInput}
placeholder={t('bulkAction.tagPlaceholder')}
ariaLabel={t('bulkAction.addAria')}
suggestions={tagSuggestions}
className="ds-inp ds-mono"
style={{ height: 26 }}
dropUp
/>
<span className="ds-seg" style={{ padding: 2, flex: 'none' }} role="group">
{(['front', 'back'] as const).map((p) => (
<button key={p} type="button" className={`ds-seg-item${position === p ? ' ds-is-active' : ''}`} style={{ height: 20, padding: '0 8px', fontSize: 11 }} aria-pressed={position === p} onClick={() => setPosition(p)}>
{t(p === 'front' ? 'bulkAction.posFront' : 'bulkAction.posBack')}
</button>
))}
</span>
<button type="button" className="ds-btn-primary" style={{ height: 26, flex: 'none' }} onClick={() => void apply('add')} disabled={opDisabled}
title={t(position === 'front' ? 'bulkAction.addFrontHint' : 'bulkAction.addBackHint')}>
{t('bulkAction.add')}
</button>
</span>
<button type="button" className="ds-ctl ds-ghost" style={{ height: 26, color: 'var(--green-text)' }} onClick={() => void apply('dedupe')} disabled={opDisabled} title={t('bulkAction.dedupeHint')}>
{t('bulkAction.dedupe')}
</button>
{selectedKeys.length < totalCount && (
<button type="button" className="ds-ctl ds-ghost" style={{ height: 26, color: 'var(--green-text)' }} onClick={onSelectAll} title={t('bulkAction.selectAllImagesHint')}>
{t('bulkAction.selectAllImages')}
</button>
)}
<button type="button" className="ds-ctl ds-ghost" style={{ height: 26, color: 'var(--green-text)' }} onClick={onClearSelection}>
{t('common.deselect')}
</button>
</div>
)
}

return (
<div className="px-2.5 py-2 flex flex-col gap-2 text-xs shrink-0 border-b border-subtle">
{/* selection summary + selection management — kept as before. */}
Expand Down Expand Up @@ -379,7 +428,7 @@ function PositionToggle({
// 不抢「添加」主按钮的颜色。
const activeStyle = {
background: 'var(--bg-canvas)',
color: 'var(--accent)',
color: 'var(--accent-text, var(--accent))',
boxShadow:
'inset 0 0 0 1px color-mix(in oklch, var(--accent) 30%, var(--border-default))',
}
Expand Down Expand Up @@ -421,9 +470,13 @@ interface TagsFieldProps {
placeholder: string
suggestions: string[]
ariaLabel?: string
className?: string
style?: React.CSSProperties
/** Open the suggestion list above the field (bar at the bottom of a card). */
dropUp?: boolean
}

function TagsField({ value, onChange, placeholder, suggestions, ariaLabel }: TagsFieldProps) {
function TagsField({ value, onChange, placeholder, suggestions, ariaLabel, className = 'input input-mono', style = { fontSize: 'var(--t-xs)' }, dropUp }: TagsFieldProps) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -456,12 +509,12 @@ function TagsField({ value, onChange, placeholder, suggestions, ariaLabel }: Tag
onFocus={() => setOpen(true)}
placeholder={placeholder}
aria-label={ariaLabel}
className="input input-mono w-full"
style={{ fontSize: 'var(--t-xs)' }}
className={className + ' w-full'}
style={style}
/>
{open && matches.length > 0 && (
<ul
className="absolute left-0 top-full mt-0.5 z-30 bg-elevated border border-subtle rounded-lg shadow-lg max-h-[180px] overflow-y-auto min-w-[200px] list-none p-1 m-0"
className={(dropUp ? 'absolute left-0 bottom-full mb-0.5' : 'absolute left-0 top-full mt-0.5') + ' z-30 bg-elevated border border-subtle rounded-lg shadow-lg max-h-[180px] overflow-y-auto min-w-[200px] list-none p-1 m-0'}
role="listbox"
>
{matches.map((s) => (
Expand Down
Loading
Loading