T25e Generative: models

ModelCard sets a model's maker, what it does, its facts and tags, and
Use unless it is in use. ModelDownloadManager lists downloads with how
much of how much, speed and time left, and Pause, Resume, Cancel,
Retry and Delete by state. ModelStatus marks a model idle, loading,
loaded with its GPU memory, or failed. HardwareMonitor sets a meter per
part beside its history. FineTuneJobCard shows a job's phase, its
epochs and its loss on training and held-out data.

AGENTS.md: after each item, cargo clean -p ely-gpui-component too;
old builds of the crate had filled the disk. The reveal rule names
layout::bring_into_view.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 13:45:17 -04:00
1 parent 092d125eda
commit 164d033ad3
12 files changed
+1291 -7

No files matched your search

+2 -2
View File
@@ -20,7 +20,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `cargo run --example gallery` opens the gallery. `-- --page <slug>` starts on a page.
- `cargo run --example gallery -- --capture <dir>` writes PNGs of every page, light and dark, top to bottom, then each page's scripted states, including windows the demos open. macOS only.
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules.
- `rm -rf target/debug/incremental` after each task item keeps the disk lean.
- `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
- `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`.
## Layout
@@ -83,7 +83,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Only gpui can build `ExternalPaths` with paths, so drop rules live in `forms::files::dropped`, where tests reach them.
- A stroke drawn with the pointer goes through `forms::pen`: a press starts it, a drag extends it, and the lift hands it over, in or out of the pad (`SignaturePad`, `generative::InpaintCanvas`).
- gpui runs key bindings before key listeners. A container takes a child's bound key through the action with `capture_action`, as `Form` takes `Submit`.
- `ScrollHandle::scroll_to_item` runs in the container's prepaint against last frame's bounds and overflow, so a call before a list's first frame does nothing. Call it from a child's prepaint once the handle has bounds, then request one frame (`navigation::editor_tabs`). A floating list reveals its cursor, and a strip its focused item, through `forms::options::reveal` and `revealer`. A focused Tab stop deeper in a scroll box sets the box's offset whole from its own painted bounds in prepaint, once per focus (`tables::header`). Never poll layout with `request_animation_frame`.
- `ScrollHandle::scroll_to_item` runs in the container's prepaint against last frame's bounds and overflow, so a call before a list's first frame does nothing. Call it from a child's prepaint once the handle has bounds, then request one frame (`navigation::editor_tabs`). A floating list reveals its cursor, and a strip its focused item, through `forms::options::reveal` and `revealer`. A focused Tab stop deeper in a scroll box sets the box's offset whole from its own painted bounds in prepaint, once per focus, through `layout::bring_into_view`. Never poll layout with `request_animation_frame`.
- gpui animations run on the wall clock; timers run on the executor's clock, which tests advance by hand. A test that needs an animation settled turns on the theme's reduced motion. Clocks read `background_executor().now()` and wake only their own view.
- Machine-read marks, QR codes and barcodes, paint `ink` on `paper`: dark on light in both themes, so cameras read them.
- A layer over pressable content that must still pass scrolls to its ancestors uses `block_mouse_except_scroll`; `occlude` hides them from scrolls too.
+5
View File
@@ -1,4 +1,5 @@
mod canvas;
mod models;
mod panel;
mod results;
mod sound;
@@ -47,5 +48,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
.child(sound::audio(window, cx))
.child(sound::voice(window, cx))
.child(sound::video(window, cx))
.child(models::cards(window, cx))
.child(models::downloads_section(window, cx))
.child(models::machine(window, cx))
.child(models::tuning(window, cx))
.into_any_element()
}
+221
View File
@@ -0,0 +1,221 @@
use ely_gpui_component::generative::{
Download, DownloadState, FineTuneJobCard, HardwareMonitor, ModelCard, ModelDownloadManager,
ModelState, ModelStatus, Reading, TunePhase,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, section, set},
};
pub fn cards(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let using = keep(
"gen-model-used",
|| SharedString::from("orchid"),
window,
cx,
);
let now = using.read(cx).clone();
let card = |key: &'static str, name: &str, maker: &str, about: &str, size: &str| {
let pick = using.clone();
ModelCard::new(
SharedString::from(format!("gen-model-{key}")),
name.to_string(),
maker.to_string(),
about.to_string(),
)
.fact("Parameters", size.to_string())
.fact("Context", "128K tokens")
.fact("License", "Apache-2.0")
.in_use(now == key)
.on_use(move |_, cx| set(&pick, SharedString::from(key), cx))
};
section(
"ModelCard",
"A model to choose: its maker, what it does, facts such as its size and license, and what it can do. The one in use says so; the others offer Use.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_4()
.child(div().w(px(340.)).child(card("orchid", "Orchid 8B", "Orchid Labs", "Writes and reasons in plain words; runs on one card.", "8B").tag("Tools").tag("Vision")))
.child(div().w(px(340.)).child(card("juniper", "Juniper 70B", "Juniper Co", "Long, careful answers for research and code.", "70B").tag("Tools"))),
)
}
fn downloads() -> Vec<Download> {
let download = |key: &str, name: &str, bytes: u64, state| Download {
key: key.to_string().into(),
name: name.to_string().into(),
bytes,
state,
};
vec![
download(
"orchid",
"Orchid 8B · Q4",
4_900_000_000,
DownloadState::Fetching(0.43, 38_000_000),
),
download(
"juniper",
"Juniper 70B · Q4",
39_000_000_000,
DownloadState::Paused(0.12),
),
download(
"voice",
"Iris voice pack",
310_000_000,
DownloadState::Queued,
),
download(
"embed",
"Lichen embeddings",
640_000_000,
DownloadState::Failed("the disk is full".into()),
),
download("tiny", "Orchid 1B · Q8", 1_300_000_000, DownloadState::Done),
]
}
pub fn downloads_section(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let list = keep("gen-downloads", downloads, window, cx);
let now = list.read(cx).clone();
let change = |list: &gpui::Entity<Vec<Download>>| {
let list = list.clone();
move |key: &SharedString, state: Option<DownloadState>, cx: &mut App| {
let mut next = list.read(cx).clone();
match state {
Some(state) => {
let download = next
.iter_mut()
.find(|download| &download.key == key)
.expect("a listed download");
download.state = state;
}
None => next.retain(|download| &download.key != key),
}
set(&list, next, cx)
}
};
let (pause, resume, cancel, retry, delete) = (
change(&list),
change(&list),
change(&list),
change(&list),
change(&list),
);
let (at_pause, at_resume) = (list.clone(), list.clone());
section(
"ModelDownloadManager",
"Models on their way to this machine: how much of how much, how fast and how long is left, with Pause and Resume, Cancel, Retry, and Delete once a model is here.",
cx,
)
.child(probe(
"gen-downloads",
div().w(px(480.)).child(
ModelDownloadManager::new("gen-downloads", now)
.on_pause(move |key, _, cx| {
let at = held(&at_pause, key, cx);
pause(key, Some(DownloadState::Paused(at)), cx)
})
.on_resume(move |key, _, cx| {
let at = held(&at_resume, key, cx);
resume(key, Some(DownloadState::Fetching(at, 38_000_000)), cx)
})
.on_cancel(move |key, _, cx| cancel(key, None, cx))
.on_retry(move |key, _, cx| retry(key, Some(DownloadState::Queued), cx))
.on_delete(move |key, _, cx| delete(key, None, cx)),
),
))
}
/// The share a download has fetched, or none yet.
fn held(list: &gpui::Entity<Vec<Download>>, key: &SharedString, cx: &App) -> f32 {
match list
.read(cx)
.iter()
.find(|download| &download.key == key)
.map(|download| &download.state)
{
Some(DownloadState::Fetching(share, _) | DownloadState::Paused(share)) => *share,
_ => 0.0,
}
}
pub fn machine(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let history = |values: &[f32]| values.to_vec();
section(
"ModelStatus / HardwareMonitor",
"Models on this machine, loaded, loading or not, with the GPU memory each holds; and the machine at work, each part's meter turning amber then red as it fills, beside its recent history.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_6()
.child(
div()
.w(px(320.))
.flex()
.flex_col()
.gap_4()
.child(ModelStatus::new("gen-status-orchid", "Orchid 8B", ModelState::Ready).memory(7_800_000_000, 24_000_000_000))
.child(ModelStatus::new("gen-status-juniper", "Juniper 70B", ModelState::Loading(0.62)))
.child(ModelStatus::new("gen-status-iris", "Iris voice", ModelState::Idle)),
)
.child(
div().w(px(360.)).child(HardwareMonitor::new(
"gen-machine",
[
Reading { name: "GPU".into(), share: 0.72, detail: "72%".into(), history: history(&[0.2, 0.35, 0.6, 0.55, 0.7, 0.8, 0.72]) },
Reading { name: "GPU memory".into(), share: 0.93, detail: "22.3 GB of 24 GB".into(), history: history(&[0.5, 0.6, 0.8, 0.9, 0.92, 0.93, 0.93]) },
Reading { name: "CPU".into(), share: 0.31, detail: "31%".into(), history: history(&[0.4, 0.3, 0.25, 0.35, 0.3, 0.28, 0.31]) },
],
)),
),
)
}
pub fn tuning(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let canceled = keep("gen-tune-canceled", || false, window, cx);
let stopped = *canceled.read(cx);
let phase = if stopped {
TunePhase::Canceled
} else {
TunePhase::Running(2, 3, 0.64)
};
section(
"FineTuneJobCard",
"A fine-tuning job: the base model and data it learns from, where it stands, how far it has come by epoch, and its loss on training and held-out data.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_4()
.child(
div().w(px(380.)).child(
FineTuneJobCard::new("gen-tune", "Plain-words tone", "Orchid 8B", "notes-2026.jsonl · 1,240 examples", phase)
.loss(vec![1.9, 1.4, 1.1, 0.92, 0.81, 0.74], vec![2.0, 1.5, 1.2, 1.02, 0.95, 0.91])
.on_cancel(move |_, cx| set(&canceled, true, cx)),
),
)
.child(
div().w(px(380.)).child(
FineTuneJobCard::new("gen-tune-done", "Support replies", "Juniper 70B", "tickets.jsonl · 8,900 examples", TunePhase::Done)
.loss(vec![1.6, 1.1, 0.8, 0.62, 0.55, 0.51], vec![1.7, 1.2, 0.9, 0.74, 0.69, 0.68])
.on_open(|_, _| log::info!("gallery: open the tuned model")),
),
),
)
}
+297
View File
@@ -0,0 +1,297 @@
use std::rc::Rc;
use gpui::{
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
};
use crate::{
buttons::{Button, ButtonVariant, IconButton},
forms::OnValue,
motion::ProgressBar,
primitives::IconName,
theme::{ActiveTheme, ControlSize, TextSize},
typography::{
Ellipsis,
format::{DurationStyle, duration, file_size, percent},
},
};
/// Where a model's download stands.
#[derive(Clone, Debug, PartialEq)]
pub enum DownloadState {
Queued,
/// Under way: the share fetched, and bytes a second.
Fetching(f32, u64),
Paused(f32),
Done,
Failed(SharedString),
}
/// One model to fetch: its key, its name, its size in bytes, and where it stands.
#[derive(Clone, Debug, PartialEq)]
pub struct Download {
pub key: SharedString,
pub name: SharedString,
pub bytes: u64,
pub state: DownloadState,
}
/// What a download's state says under its name: how much of how much, how fast, and how long is left.
pub(crate) fn status(download: &Download) -> String {
let of = |share: f32| {
let done = (download.bytes as f64 * f64::from(share)) as u64;
format!(
"{} of {}",
file_size(done, false),
file_size(download.bytes, false)
)
};
match &download.state {
DownloadState::Queued => "Queued".to_string(),
DownloadState::Fetching(share, 0) => of(*share),
DownloadState::Fetching(share, speed) => {
let left = (download.bytes as f64 * f64::from(1.0 - share)) as u64 / speed;
format!(
"{} · {}/s · about {} left",
of(*share),
file_size(*speed, false),
duration(left.max(1), DurationStyle::Compact)
)
}
DownloadState::Paused(share) => format!(
"Paused at {} · {}",
percent(f64::from(*share), 0, false),
of(*share)
),
DownloadState::Done => file_size(download.bytes, false),
DownloadState::Failed(reason) => format!("Failed · {reason}"),
}
}
/// Models on their way to this machine: each row its name, how much of how much with speed and time left, a bar while it comes; Pause and Resume, Cancel, Retry once it fails, and Delete once it is here.
#[derive(IntoElement)]
pub struct ModelDownloadManager {
id: ElementId,
downloads: Vec<Download>,
on_pause: Option<OnValue>,
on_resume: Option<OnValue>,
on_cancel: Option<OnValue>,
on_retry: Option<OnValue>,
on_delete: Option<OnValue>,
}
impl ModelDownloadManager {
pub fn new(id: impl Into<ElementId>, downloads: impl IntoIterator<Item = Download>) -> Self {
let downloads: Vec<Download> = downloads.into_iter().collect();
for download in &downloads {
if let DownloadState::Fetching(share, _) | DownloadState::Paused(share) = download.state
{
assert!(
(0.0..=1.0).contains(&share),
"download {} at {share} of 1",
download.key
);
}
}
Self {
id: id.into(),
downloads,
on_pause: None,
on_resume: None,
on_cancel: None,
on_retry: None,
on_delete: None,
}
}
pub fn on_pause(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_pause = Some(Rc::new(handler));
self
}
pub fn on_resume(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_resume = Some(Rc::new(handler));
self
}
pub fn on_cancel(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_cancel = Some(Rc::new(handler));
self
}
pub fn on_retry(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_retry = Some(Rc::new(handler));
self
}
/// Gets a downloaded model to take off this machine.
pub fn on_delete(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_delete = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ModelDownloadManager {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let icon = |key: &SharedString,
what: &'static str,
glyph: IconName,
tip: &'static str,
handler: &Option<OnValue>| {
handler.clone().map(|handler| {
let key = key.clone();
IconButton::new((self.id.clone(), format!("{what}-{key}")), glyph)
.size(ControlSize::Sm)
.tooltip(tip)
.on_click(move |_, window, cx| {
log::info!("model downloads: {what} {key}");
handler(&key, window, cx)
})
.into_any_element()
})
};
let rows = self.downloads.iter().map(|download| {
let key = &download.key;
let actions: Vec<_> = match &download.state {
DownloadState::Fetching(..) => vec![
icon(key, "pause", IconName::Pause, "Pause", &self.on_pause),
icon(key, "cancel", IconName::X, "Cancel", &self.on_cancel),
],
DownloadState::Paused(_) => vec![
icon(key, "resume", IconName::Play, "Resume", &self.on_resume),
icon(key, "cancel", IconName::X, "Cancel", &self.on_cancel),
],
DownloadState::Queued => {
vec![icon(key, "cancel", IconName::X, "Cancel", &self.on_cancel)]
}
DownloadState::Failed(_) => vec![
self.on_retry.clone().map(|retry| {
let key = key.clone();
Button::new((self.id.clone(), format!("retry-{key}")), "Retry")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, window, cx| {
log::info!("model downloads: retry {key}");
retry(&key, window, cx)
})
.into_any_element()
}),
icon(key, "delete", IconName::X, "Remove", &self.on_delete),
],
DownloadState::Done => vec![icon(
key,
"delete",
IconName::Trash2,
"Delete from this machine",
&self.on_delete,
)],
};
let bar = match download.state {
DownloadState::Fetching(share, _) | DownloadState::Paused(share) => Some(
ProgressBar::new((self.id.clone(), format!("bar-{key}")), share),
),
_ => None,
};
let failed = matches!(download.state, DownloadState::Failed(_));
div()
.w_full()
.flex()
.flex_col()
.gap_2()
.py_2()
.child(
div()
.flex()
.items_center()
.gap_3()
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.gap_0p5()
.child(
div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(colors.fg)
.child(Ellipsis::new(download.name.clone())),
)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(if failed {
colors.danger
} else {
colors.fg_muted
})
.child(Ellipsis::new(status(download))),
),
)
.child(
div()
.flex_none()
.flex()
.items_center()
.gap_1()
.children(actions.into_iter().flatten()),
),
)
.children(bar)
});
div().w_full().flex().flex_col().children(rows)
}
}
#[cfg(test)]
mod tests {
use super::{Download, DownloadState, status};
fn download(state: DownloadState) -> Download {
Download {
key: "model".into(),
name: "Orchid 8B".into(),
bytes: 4_000_000_000,
state,
}
}
#[test]
fn a_status_says_how_much_how_fast_and_how_long() {
let fetching = download(DownloadState::Fetching(0.25, 40_000_000));
assert_eq!(
status(&fetching),
"1.0 GB of 4.0 GB · 40 MB/s · about 1m 15s left"
);
assert_eq!(
status(&download(DownloadState::Fetching(0.5, 0))),
"2.0 GB of 4.0 GB"
);
assert_eq!(
status(&download(DownloadState::Paused(0.5))),
"Paused at 50% · 2.0 GB of 4.0 GB"
);
assert_eq!(status(&download(DownloadState::Done)), "4.0 GB");
assert_eq!(
status(&download(DownloadState::Failed("disk full".into()))),
"Failed · disk full"
);
}
}
+77
View File
@@ -0,0 +1,77 @@
use gpui::{
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::*,
};
use crate::{
data_display::{Meter, Sparkline},
theme::ActiveTheme,
};
/// One part of the machine: its name, how full it is from 0 to 1, that in units, and its recent shares, oldest first.
#[derive(Clone, Debug, PartialEq)]
pub struct Reading {
pub name: SharedString,
pub share: f32,
pub detail: SharedString,
pub history: Vec<f32>,
}
/// The machine at work: a meter for each part, as GPU, its memory and the CPU, that turns amber then red as it fills, and beside it a line of its recent history.
#[derive(IntoElement)]
pub struct HardwareMonitor {
id: ElementId,
readings: Vec<Reading>,
}
impl HardwareMonitor {
pub fn new(id: impl Into<ElementId>, readings: impl IntoIterator<Item = Reading>) -> Self {
let readings: Vec<Reading> = readings.into_iter().collect();
for reading in &readings {
let within = |share: &f32| (0.0..=1.0).contains(share);
assert!(
within(&reading.share) && reading.history.iter().all(within),
"{} reads outside 0 to 1",
reading.name
);
}
Self {
id: id.into(),
readings,
}
}
}
impl RenderOnce for HardwareMonitor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let spark = cx.theme().spark_size();
div().w_full().flex().flex_col().gap_3().children(
self.readings.into_iter().enumerate().map(|(ix, reading)| {
div()
.flex()
.items_end()
.gap_3()
.child(
div().flex_1().min_w_0().child(
Meter::new(
(self.id.clone(), format!("meter-{ix}")),
reading.name,
reading.share,
)
.detail(reading.detail)
.thresholds(0.75, 0.9),
),
)
.when(reading.history.len() > 1, |row| {
row.child(
div()
.flex_none()
.w(spark.width)
.h(spark.height)
.child(Sparkline::new(reading.history).area()),
)
})
}),
)
}
}
+8
View File
@@ -1,8 +1,11 @@
mod audio;
mod brush;
mod compare;
mod downloads;
mod grid;
mod hardware;
mod mask;
mod models;
mod prompt;
mod queue;
mod ratio;
@@ -10,6 +13,7 @@ mod seed;
mod style;
#[cfg(all(test, feature = "test-support"))]
mod tests;
mod tuning;
mod variation;
mod video;
mod voice;
@@ -17,13 +21,17 @@ mod voice;
pub use audio::AudioGenerationPlayer;
pub use brush::MaskBrush;
pub use compare::{ABCompareView, Verdict};
pub use downloads::{Download, DownloadState, ModelDownloadManager};
pub use grid::{GenerationGrid, Outcome};
pub use hardware::{HardwareMonitor, Reading};
pub use mask::{InpaintCanvas, MaskStroke};
pub use models::{ModelCard, ModelState, ModelStatus};
pub use prompt::PromptEnhancer;
pub use queue::{GenerationQueue, Job, JobState};
pub use ratio::AspectRatioPicker;
pub use seed::SeedInput;
pub use style::{StylePreset, StylePresetPicker};
pub use tuning::{FineTuneJobCard, TunePhase};
pub use variation::VariationPicker;
pub use video::{Shot, VideoGenerationTimeline};
pub use voice::{TTSVoicePicker, Voice};
+303
View File
@@ -0,0 +1,303 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, div, prelude::*,
};
use crate::{
buttons::{Button, ButtonVariant},
data_display::{Badge, DescriptionList, Meter, Tone},
forms::Run,
motion::{ProgressBar, Spinner},
primitives::Severity,
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
typography::{
Ellipsis,
format::{file_size, percent},
},
};
/// A model to choose: its name and maker, a line on what it does, facts such as its size and license, tags for what it can do, and Use unless it is in use.
#[derive(IntoElement)]
pub struct ModelCard {
id: ElementId,
name: SharedString,
maker: SharedString,
about: SharedString,
facts: Vec<(SharedString, SharedString)>,
tags: Vec<SharedString>,
in_use: bool,
on_use: Option<Run>,
}
impl ModelCard {
pub fn new(
id: impl Into<ElementId>,
name: impl Into<SharedString>,
maker: impl Into<SharedString>,
about: impl Into<SharedString>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
maker: maker.into(),
about: about.into(),
facts: Vec::new(),
tags: Vec::new(),
in_use: false,
on_use: None,
}
}
/// A fact and its value, such as "Parameters" and "8B".
pub fn fact(mut self, label: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
self.facts.push((label.into(), value.into()));
self
}
/// Something it can do, such as "Vision".
pub fn tag(mut self, tag: impl Into<SharedString>) -> Self {
self.tags.push(tag.into());
self
}
pub fn in_use(mut self, in_use: bool) -> Self {
self.in_use = in_use;
self
}
pub fn on_use(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_use = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ModelCard {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let label = theme.label_width();
let facts = self
.facts
.into_iter()
.fold(DescriptionList::new(), |list, (label, value)| {
list.item(label, value)
});
let (name, in_use) = (self.name.clone(), self.in_use);
let choose = self.on_use.filter(|_| !in_use).map(|choose| {
Button::new((self.id.clone(), "use"), "Use")
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
.on_click(move |_, window, cx| {
log::info!("model card: use {name}");
choose(window, cx)
})
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border)
.bg(colors.surface)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_x_3()
.gap_y_2()
.child(
div()
.flex_1()
.min_w(label)
.flex()
.flex_col()
.child(
div()
.text_size(theme.text_size(TextSize::Base))
.font_weight(FontWeight::SEMIBOLD)
.text_color(colors.fg)
.child(Ellipsis::new(self.name)),
)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_muted)
.child(Ellipsis::new(self.maker)),
),
)
.child(
div()
.flex_none()
.children(in_use.then(|| Badge::new("In use").tone(Tone::Success)))
.children(choose),
),
)
.child(
div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(colors.fg_muted)
.child(self.about),
)
.child(facts)
.when(!self.tags.is_empty(), |card| {
card.child(
div()
.flex()
.flex_wrap()
.gap_1()
.children(self.tags.into_iter().map(Badge::new)),
)
})
}
}
/// Where a model stands on this machine.
#[derive(Clone, Debug, PartialEq)]
pub enum ModelState {
Idle,
Loading(f32),
Ready,
Failed(SharedString),
}
impl ModelState {
fn word(&self) -> String {
match self {
Self::Idle => "Not loaded".to_string(),
Self::Loading(share) => format!("Loading · {}", percent(f64::from(*share), 0, false)),
Self::Ready => "Loaded".to_string(),
Self::Failed(_) => "Failed".to_string(),
}
}
}
/// A model on this machine: a mark and a word for where it stands, a bar while it loads, and once loaded, the GPU memory it holds.
#[derive(IntoElement)]
pub struct ModelStatus {
id: ElementId,
name: SharedString,
state: ModelState,
memory: Option<(u64, u64)>,
}
impl ModelStatus {
pub fn new(id: impl Into<ElementId>, name: impl Into<SharedString>, state: ModelState) -> Self {
if let ModelState::Loading(share) = state {
assert!(
(0.0..=1.0).contains(&share),
"a model loaded to {share} of 1"
);
}
Self {
id: id.into(),
name: name.into(),
state,
memory: None,
}
}
/// GPU memory it holds and the card's whole, in bytes.
pub fn memory(mut self, used: u64, total: u64) -> Self {
assert!(total > 0 && used <= total, "GPU memory {used} of {total}");
self.memory = Some((used, total));
self
}
}
impl RenderOnce for ModelStatus {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let tone = match self.state {
ModelState::Idle => None,
ModelState::Loading(_) => Some(Severity::Info),
ModelState::Ready => Some(Severity::Success),
ModelState::Failed(_) => Some(Severity::Danger),
};
let ink = tone.map_or(colors.fg_subtle, |tone| tone.color(&colors));
let mark = match self.state {
ModelState::Loading(_) => Spinner::new((self.id.clone(), "loading"))
.size(IconSize::Xs)
.color(ink)
.into_any_element(),
_ => div()
.size(theme.status_dot())
.rounded_full()
.when(tone.is_some(), |dot| dot.bg(ink))
.when(tone.is_none(), |dot| dot.border_1().border_color(ink))
.into_any_element(),
};
let below = match (&self.state, self.memory) {
(ModelState::Loading(share), _) => {
Some(ProgressBar::new((self.id.clone(), "load"), *share).into_any_element())
}
(ModelState::Failed(reason), _) => Some(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(ink)
.child(Ellipsis::new(reason.clone()))
.into_any_element(),
),
(ModelState::Ready, Some((used, total))) => Some(
Meter::new(
(self.id.clone(), "memory"),
"GPU memory",
used as f32 / total as f32,
)
.detail(format!(
"{} of {}",
file_size(used, false),
file_size(total, false)
))
.into_any_element(),
),
_ => None,
};
div()
.w_full()
.flex()
.flex_col()
.gap_2()
.text_size(theme.text_size(TextSize::Sm))
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex_none()
.w(theme.icon_size(IconSize::Xs))
.flex()
.justify_center()
.child(mark),
)
.child(
div()
.flex_1()
.min_w_0()
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg)
.child(Ellipsis::new(self.name)),
)
.child(
div()
.flex_none()
.text_size(theme.text_size(TextSize::Xs))
.text_color(if tone == Some(Severity::Danger) {
ink
} else {
colors.fg_muted
})
.child(self.state.word()),
),
)
.children(below)
}
}
+1
View File
@@ -3,6 +3,7 @@ use gpui::{KeyBinding, KeyUpEvent, Keystroke, TestAppContext, VisualTestContext}
use crate::{forms, primitives::FocusNext, theme::Theme};
mod canvas;
mod models;
mod results;
mod settings;
mod sound;
+139
View File
@@ -0,0 +1,139 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{Context, IntoElement, ParentElement, Render, Styled, TestAppContext, Window, div, px};
use super::{press, settle, setup, tab};
use crate::generative::{
Download, DownloadState, FineTuneJobCard, HardwareMonitor, ModelCard, ModelDownloadManager,
ModelState, ModelStatus, Reading, TunePhase,
};
type Heard = Rc<RefCell<Vec<String>>>;
/// Downloads in each state, and the actions heard.
struct Downloads(Heard);
impl Render for Downloads {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let download = |key: &str, state| Download {
key: key.to_string().into(),
name: key.to_string().into(),
bytes: 1_000_000,
state,
};
let heard = |what: &'static str| {
let heard = self.0.clone();
move |key: &gpui::SharedString, _: &mut Window, _: &mut gpui::App| {
heard.borrow_mut().push(format!("{what} {key}"))
}
};
div().w(px(420.0)).child(
ModelDownloadManager::new(
"downloads",
[
download("a", DownloadState::Fetching(0.5, 1_000)),
download("b", DownloadState::Paused(0.2)),
download("c", DownloadState::Failed("disk full".into())),
download("d", DownloadState::Done),
],
)
.on_pause(heard("pause"))
.on_resume(heard("resume"))
.on_cancel(heard("cancel"))
.on_retry(heard("retry"))
.on_delete(heard("delete")),
)
}
}
#[gpui::test]
fn each_download_offers_what_its_state_allows(cx: &mut TestAppContext) {
setup(cx);
let heard = Heard::default();
let store = heard.clone();
let (_, cx) = cx.add_window_view(|_, _| Downloads(store));
cx.update(|window, _| window.activate_window());
settle(cx);
for _ in 0..7 {
tab(1, cx);
press("enter", cx);
}
assert_eq!(
*heard.borrow(),
[
"pause a", "cancel a", "resume b", "cancel b", "retry c", "delete c", "delete d"
]
);
}
/// A model to use and one in use, a job training for one epoch and one done, and what they heard.
struct Cards(Heard);
impl Render for Cards {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (used, canceled, opened) = (self.0.clone(), self.0.clone(), self.0.clone());
div()
.w(px(420.0))
.child(
ModelCard::new("free", "Orchid", "Orchid Labs", "A model")
.on_use(move |_, _| used.borrow_mut().push("use".into())),
)
.child(
ModelCard::new("busy", "Juniper", "Juniper Co", "A model")
.in_use(true)
.on_use(|_, _| {}),
)
.child(
FineTuneJobCard::new(
"training",
"tone",
"Orchid",
"notes",
TunePhase::Running(1, 1, 0.5),
)
.on_cancel(move |_, _| canceled.borrow_mut().push("cancel".into())),
)
.child(
FineTuneJobCard::new("done", "tone", "Orchid", "notes", TunePhase::Done)
.loss(vec![1.2, 0.8, 0.6], vec![1.3, 0.9, 0.7])
.on_open(move |_, _| opened.borrow_mut().push("open".into())),
)
}
}
#[gpui::test]
fn cards_offer_what_their_state_allows(cx: &mut TestAppContext) {
setup(cx);
let heard = Heard::default();
let store = heard.clone();
let (_, cx) = cx.add_window_view(|_, _| Cards(store));
cx.update(|window, _| window.activate_window());
settle(cx);
for _ in 0..3 {
tab(1, cx);
press("enter", cx);
}
assert_eq!(
*heard.borrow(),
["use", "cancel", "open"],
"a model in use offers no Use"
);
}
#[test]
#[should_panic(expected = "GPU memory 5 of 4")]
fn memory_past_the_card_fails_loud() {
let _ = ModelStatus::new("model", "Orchid", ModelState::Ready).memory(5, 4);
}
#[test]
#[should_panic(expected = "GPU reads outside 0 to 1")]
fn a_reading_past_full_fails_loud() {
let reading = Reading {
name: "GPU".into(),
share: 1.2,
detail: "120%".into(),
history: Vec::new(),
};
let _ = HardwareMonitor::new("machine", [reading]);
}
+230
View File
@@ -0,0 +1,230 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, div,
};
use crate::{
buttons::{Button, ButtonVariant},
charts::{LineChart, Series},
data_display::{Badge, Tone},
forms::Run,
motion::ProgressBar,
theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::{Ellipsis, format::percent, tabular},
};
/// Where a fine-tuning job stands.
#[derive(Clone, Debug, PartialEq)]
pub enum TunePhase {
/// Waiting, with this many ahead of it.
Queued(usize),
/// Training: the epoch under way from one, of how many, and the share of it done.
Running(usize, usize, f32),
Done,
Failed(SharedString),
Canceled,
}
impl TunePhase {
fn badge(&self) -> (&'static str, Tone) {
match self {
Self::Queued(_) => ("Queued", Tone::Neutral),
Self::Running(..) => ("Training", Tone::Info),
Self::Done => ("Done", Tone::Success),
Self::Failed(_) => ("Failed", Tone::Danger),
Self::Canceled => ("Canceled", Tone::Neutral),
}
}
}
/// A fine-tuning job: its name, the base model and data it learns from, where it stands, how far it has come by epoch, and its loss on training and held-out data. Cancel while it waits or trains; Open once done.
#[derive(IntoElement)]
pub struct FineTuneJobCard {
id: ElementId,
name: SharedString,
base: SharedString,
data: SharedString,
phase: TunePhase,
train: Vec<f64>,
held: Vec<f64>,
on_cancel: Option<Run>,
on_open: Option<Run>,
}
impl FineTuneJobCard {
pub fn new(
id: impl Into<ElementId>,
name: impl Into<SharedString>,
base: impl Into<SharedString>,
data: impl Into<SharedString>,
phase: TunePhase,
) -> Self {
if let TunePhase::Running(epoch, epochs, share) = phase {
assert!(
(1..=epochs).contains(&epoch) && (0.0..=1.0).contains(&share),
"epoch {epoch} of {epochs} at {share}"
);
}
Self {
id: id.into(),
name: name.into(),
base: base.into(),
data: data.into(),
phase,
train: Vec::new(),
held: Vec::new(),
on_cancel: None,
on_open: None,
}
}
/// Loss at each checkpoint, on training data and on held-out data, one value each.
pub fn loss(mut self, train: impl Into<Vec<f64>>, held: impl Into<Vec<f64>>) -> Self {
let (train, held) = (train.into(), held.into());
assert_eq!(
train.len(),
held.len(),
"a loss for each checkpoint on both sets"
);
self.train = train;
self.held = held;
self
}
pub fn on_cancel(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_cancel = Some(Rc::new(handler));
self
}
pub fn on_open(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_open = Some(Rc::new(handler));
self
}
}
impl RenderOnce for FineTuneJobCard {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let (word, tone) = self.phase.badge();
let live = matches!(self.phase, TunePhase::Queued(_) | TunePhase::Running(..));
let action = |key: &'static str, label: &'static str, run: Option<Run>| {
run.map(|run| {
Button::new((self.id.clone(), key), label)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, window, cx| {
log::info!("fine-tune job: {key}");
run(window, cx)
})
})
};
let actions = match self.phase {
TunePhase::Done => action("open", "Open", self.on_open),
_ if live => action("cancel", "Cancel", self.on_cancel),
_ => None,
};
let progress = match &self.phase {
TunePhase::Running(epoch, epochs, share) => {
let whole = ((*epoch - 1) as f32 + share) / *epochs as f32;
Some(
div()
.flex()
.flex_col()
.gap_1p5()
.child(match *epochs {
1 => ProgressBar::new((self.id.clone(), "epochs"), whole),
_ => ProgressBar::new((self.id.clone(), "epochs"), whole)
.segments(*epochs),
})
.child(
tabular(div())
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_muted)
.child(format!(
"Epoch {epoch} of {epochs} · {}",
percent(f64::from(*share), 0, false)
)),
)
.into_any_element(),
)
}
TunePhase::Queued(ahead) => Some(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_muted)
.child(format!("{ahead} ahead"))
.into_any_element(),
),
TunePhase::Failed(reason) => Some(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.danger)
.child(reason.clone())
.into_any_element(),
),
_ => None,
};
let chart = (self.train.len() > 1).then(|| {
let steps: Vec<SharedString> = (1..=self.train.len())
.map(|step| step.to_string().into())
.collect();
LineChart::new((self.id.clone(), "loss"), steps)
.series(Series::new("Training", self.train))
.series(Series::new("Held out", self.held))
.format(|value| format!("{value:.2}"))
.h(theme.generative().loss)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border)
.bg(colors.surface)
.child(
div()
.flex()
.flex_wrap()
.items_start()
.gap_x_3()
.gap_y_2()
.child(
div()
.flex_1()
.min_w(theme.label_width())
.flex()
.flex_col()
.child(
div()
.text_size(theme.text_size(TextSize::Sm))
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg)
.child(Ellipsis::new(self.name)),
)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_muted)
.child(Ellipsis::new(format!("{} · {}", self.base, self.data))),
),
)
.child(
div()
.flex_none()
.flex()
.items_center()
.gap_2()
.child(Badge::new(word).tone(tone))
.children(actions),
),
)
.children(progress)
.children(chart)
}
}
+3
View File
@@ -11,6 +11,8 @@ pub struct GenerativeSizes {
pub second: Rems,
/// A shot's height on a timeline.
pub shot: Rems,
/// A loss chart's height on a job's card.
pub loss: Rems,
}
impl Theme {
@@ -19,6 +21,7 @@ impl Theme {
preset: px_to_rems(88.0),
second: px_to_rems(24.0),
shot: px_to_rems(56.0),
loss: px_to_rems(140.0),
}
}
}
+5 -5
View File
@@ -238,11 +238,11 @@
## T25e Generative · Models
- [ ] ModelCard
- [ ] ModelDownloadManager
- [ ] ModelStatus
- [ ] HardwareMonitor
- [ ] FineTuneJobCard
- [x] ModelCard
- [x] ModelDownloadManager
- [x] ModelStatus
- [x] HardwareMonitor
- [x] FineTuneJobCard
## T25f Generative · Data and prompts