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
61 changes: 39 additions & 22 deletions ui/src/dash/stacks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,28 +488,45 @@ function StackDrawer({ mode, source, existing = [], onClose, onSaved }) {
{v.slots.map((s, i) => {
const isNew = !!s.slot && !liveSlots.some(ls => ls.name === s.slot);
return (
<div className="st-slot-edit" key={i}>
<input className="pf-input mono st-slot-name" value={s.slot} list="st-existing-slots"
onChange={e => setSlot(i, 'slot', e.target.value)} placeholder="pick or name…" maxLength={32}
title={isNew ? 'New slot — created on apply' : 'Existing slot'} data-testid={`st-slot-name-${i}`} />
{isNew && <span className="st-slot-new mono" title="Created on apply">new</span>}
<select className="pf-input mono st-slot-model" value={s.model}
onChange={e => setSlot(i, 'model', e.target.value)} data-testid={`st-slot-model-${i}`}>
<option value="">— model —</option>
{models.map(m => <option key={m.id} value={m.id}>{m.id}</option>)}
</select>
<select className="pf-input mono st-slot-dev" value={s.device} onChange={e => setSlot(i, 'device', e.target.value)}>
{DEVICES.map(d => <option key={d} value={d}>{DEVICE_META[d].label}</option>)}
</select>
<select className="pf-input mono st-slot-prof" value={s.profile} onChange={e => setSlot(i, 'profile', e.target.value)}>
<option value="">— profile —</option>
{profiles.map(p => <option key={p.name} value={p.name}>{p.name}</option>)}
</select>
<button type="button" className={'pf-switch sm' + (s.mtp ? ' on' : '')}
onClick={() => setSlot(i, 'mtp', !s.mtp)} role="switch" aria-checked={s.mtp} title="MTP speculative decode">
<span className="pf-switch-knob" /><span className="mono">MTP</span>
</button>
<button type="button" className="pf-btn danger" onClick={() => rmSlot(i)} title="Remove slot" data-testid={`st-slot-rm-${i}`}>{Icons.trash}</button>
<div className="st-slot-card" key={i}>
<div className="st-slot-head">
<input className="pf-input mono st-slot-name" value={s.slot} list="st-existing-slots"
onChange={e => setSlot(i, 'slot', e.target.value)} placeholder="slot name — pick or type…" maxLength={32}
title={isNew ? 'New slot — created on apply' : 'Existing slot'} data-testid={`st-slot-name-${i}`} />
{isNew && <span className="st-slot-new mono" title="Created on apply">new</span>}
<span className="pf-grow" />
<button type="button" className="pf-btn danger st-slot-rm" onClick={() => rmSlot(i)} title="Remove slot" data-testid={`st-slot-rm-${i}`}>{Icons.trash}</button>
</div>
<div className="st-slot-fields">
<label className="st-fld st-fld-model">
<span className="st-fld-lbl">Model</span>
<select className="pf-input mono" value={s.model}
onChange={e => setSlot(i, 'model', e.target.value)} data-testid={`st-slot-model-${i}`}>
<option value="">— model —</option>
{models.map(m => <option key={m.id} value={m.id}>{m.id}</option>)}
</select>
</label>
<label className="st-fld">
<span className="st-fld-lbl">Device</span>
<select className="pf-input mono" value={s.device} onChange={e => setSlot(i, 'device', e.target.value)}>
{DEVICES.map(d => <option key={d} value={d}>{DEVICE_META[d].label}</option>)}
</select>
</label>
<label className="st-fld">
<span className="st-fld-lbl">Profile</span>
<select className="pf-input mono" value={s.profile} onChange={e => setSlot(i, 'profile', e.target.value)}>
<option value="">— profile —</option>
{profiles.map(p => <option key={p.name} value={p.name}>{p.name}</option>)}
</select>
</label>
<div className="st-fld st-fld-mtp">
<span className="st-fld-lbl">Speculative decode</span>
<button type="button" className={'pf-switch sm' + (s.mtp ? ' on' : '')}
onClick={() => setSlot(i, 'mtp', !s.mtp)} role="switch" aria-checked={s.mtp} title="MTP speculative decode">
<span className="pf-switch-knob" /><span className="mono">MTP</span>
</button>
</div>
</div>
</div>
);
})}
Expand Down
29 changes: 23 additions & 6 deletions ui/src/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -2924,7 +2924,7 @@ select.pf-input { cursor: pointer; }
.pf-seg-btn.on { color: var(--bk); border-color: color-mix(in srgb, var(--bk) 50%, transparent); background: color-mix(in srgb, var(--bk) 12%, transparent); }

/* MTP switch */
.pf-switch { display: inline-flex; align-items: center; gap: 9px; padding: 0; border: none; background: transparent; cursor: pointer; }
.pf-switch { position: relative; display: inline-flex; align-items: center; gap: 9px; padding: 0; border: none; background: transparent; cursor: pointer; }
.pf-switch::before { content: ""; width: 34px; height: 19px; border-radius: 999px; background: var(--bg-3); border: 1px solid var(--line); position: relative; transition: all 0.15s ease; flex-shrink: 0; }
.pf-switch.on::before { background: var(--accent-soft); border-color: var(--accent-line); }
.pf-switch-knob { position: absolute; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: var(--fg-4); transition: all 0.15s ease; pointer-events: none; }
Expand Down Expand Up @@ -3142,14 +3142,31 @@ select.pf-input { cursor: pointer; }
.st-slugrow { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.st-slugrow .pf-input { flex: 1; }

/* Editor: per-slot row. */
.st-slots-edit { margin-top: 6px; display: flex; flex-direction: column; gap: 8px; }
.st-slot-edit { display: grid; grid-template-columns: 110px 1fr 96px 1fr auto auto; gap: 6px; align-items: center; }
.st-slot-edit .pf-input { min-width: 0; }
/* Editor: per-slot labeled card. Each slot is a card — a header row (slot name
+ remove) above a two-column labeled field grid — so model/profile/device are
each fully visible and labeled, instead of crammed into one unreadable row. */
.st-slots-edit { margin-top: 6px; display: flex; flex-direction: column; gap: 10px; }
.st-slot-card {
border: 1px solid var(--line-soft); border-radius: var(--rad-md, 9px);
background: var(--bg-1); padding: 10px 11px 11px;
display: flex; flex-direction: column; gap: 9px;
}
.st-slot-head { display: flex; align-items: center; gap: 8px; }
.st-slot-head .st-slot-name { flex: 1 1 auto; min-width: 0; }
.st-slot-rm { flex: 0 0 auto; padding: 5px 8px; }
.st-slot-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px 10px; align-items: end; }
.st-fld { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.st-fld-model { grid-column: 1 / -1; } /* model spans full width — ids are long */
.st-fld-lbl {
font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
color: var(--fg-4); font-weight: 600;
}
.st-fld .pf-input, .st-fld select { width: 100%; min-width: 0; }
.st-fld-mtp .pf-switch.sm { margin-top: 2px; }
.pf-switch.sm { gap: 5px; }
.pf-switch.sm .mono { font-size: 10px; color: var(--fg-3); }
@media (max-width: 720px) {
.st-slot-edit { grid-template-columns: 1fr 1fr; }
.st-slot-fields { grid-template-columns: 1fr; }
.st-diff-row { grid-template-columns: 1fr; gap: 2px; }
}

Expand Down