Panics · P25 Generative: gone choices mark none, overruns peg; seed fields hold seeds through fits, undo and input methods
This commit is contained in:
1 parent
fb7ee42e3d
commit
68a98198b0
17 files changed
+451
-123
No files matched your search
+16
-4
@@ -180,6 +180,13 @@ impl TextInput {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// The text as committed: while an input method composes, the text before it began.
|
||||
pub(crate) fn committed(&self) -> &str {
|
||||
self.composing
|
||||
.as_ref()
|
||||
.map_or(&self.text, |start| &start.text)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
@@ -245,7 +252,10 @@ impl TextInput {
|
||||
pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
|
||||
let text = self.admit(&text.into(), 0);
|
||||
let text = match &self.fit {
|
||||
Some(fit) => fit(&self.text, 0..self.text.len(), &text).0,
|
||||
Some(fit) => {
|
||||
let base = self.committed();
|
||||
fit(base, 0..base.len(), &text).0
|
||||
}
|
||||
None => text,
|
||||
};
|
||||
self.selection = text.len()..text.len();
|
||||
@@ -256,9 +266,10 @@ impl TextInput {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Reshapes every edit, as a mask does: text, replaced range and typed text in; text and caret out.
|
||||
/// Reshapes every edit, as a mask does: text, replaced range and typed text in; text and caret out. Undo starts afresh, since earlier text need not fit.
|
||||
pub(crate) fn set_fit(&mut self, fit: Fit) {
|
||||
self.fit = Some(fit);
|
||||
self.history = History::default();
|
||||
}
|
||||
|
||||
/// Replaces the selection, as typing would. An undo step.
|
||||
@@ -353,11 +364,12 @@ impl TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// The state as committed, so undo never keeps an input method's text before it commits.
|
||||
fn snapshot(&self) -> Snapshot {
|
||||
Snapshot {
|
||||
self.composing.clone().unwrap_or_else(|| Snapshot {
|
||||
text: self.text.clone(),
|
||||
selection: self.selection.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn restore(&mut self, snapshot: Snapshot, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -80,12 +80,16 @@ impl RenderOnce for ABCompareView {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let count = self.sides.len();
|
||||
assert!((2..=4).contains(&count), "a comparison of {count} sides");
|
||||
if let Some(Verdict::Side(ix)) = self.verdict {
|
||||
assert!(ix < count, "verdict for side {ix} of {count}");
|
||||
}
|
||||
let verdict = match self.verdict {
|
||||
Some(Verdict::Side(ix)) if ix >= count => {
|
||||
log::error!("comparison: verdict for side {ix} of {count}; none given");
|
||||
None
|
||||
}
|
||||
verdict => verdict,
|
||||
};
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let decided = self.verdict.is_some();
|
||||
let decided = verdict.is_some();
|
||||
let judge = |verdict: Verdict, label: SharedString, key: String| {
|
||||
self.on_verdict.clone().map(|judge| {
|
||||
Button::new((self.id.clone(), key), label)
|
||||
@@ -103,7 +107,7 @@ impl RenderOnce for ABCompareView {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (name, answer))| {
|
||||
let won = self.verdict == Some(Verdict::Side(ix));
|
||||
let won = verdict == Some(Verdict::Side(ix));
|
||||
let title = heading(ix, &name, self.blind, decided);
|
||||
titles.push(title.clone());
|
||||
div()
|
||||
@@ -147,7 +151,7 @@ impl RenderOnce for ABCompareView {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let verdicts = match self.verdict {
|
||||
let verdicts = match verdict {
|
||||
Some(Verdict::Tie) => vec![div().child("Called a tie").into_any_element()],
|
||||
Some(Verdict::Side(_)) => Vec::new(),
|
||||
None => titles
|
||||
|
||||
+15
-7
@@ -63,14 +63,22 @@ impl EmbeddingVisualizer {
|
||||
for (ix, name) in groups.iter().enumerate() {
|
||||
assert!(!groups[..ix].contains(name), "group {name} named twice");
|
||||
}
|
||||
let points: Vec<Embedded> = points
|
||||
.into_iter()
|
||||
.filter(|point| {
|
||||
let held = point.group < groups.len();
|
||||
if !held {
|
||||
log::error!(
|
||||
"embedding: point {} in group {} of {}; left out",
|
||||
point.key,
|
||||
point.group,
|
||||
groups.len()
|
||||
);
|
||||
}
|
||||
held
|
||||
})
|
||||
.collect();
|
||||
for point in &points {
|
||||
assert!(
|
||||
point.group < groups.len(),
|
||||
"point {} in group {} of {}",
|
||||
point.key,
|
||||
point.group,
|
||||
groups.len()
|
||||
);
|
||||
assert!(
|
||||
point.at.iter().all(|axis| (-1.0..=1.0).contains(axis)),
|
||||
"point {} sits outside -1 to 1",
|
||||
|
||||
@@ -225,13 +225,16 @@ fn shaped(
|
||||
}
|
||||
}
|
||||
|
||||
/// `at` as shares of `bounds`.
|
||||
/// `at` as shares of `bounds`, kept on the picture as the pen keeps its points in the pad; a canvas that shrank mid-stroke ends the stroke at its edge.
|
||||
fn share(bounds: Bounds<Pixels>, at: Point<Pixels>) -> (f32, f32) {
|
||||
let (width, height) = (
|
||||
f32::from(bounds.size.width).max(1.0),
|
||||
f32::from(bounds.size.height).max(1.0),
|
||||
);
|
||||
(f32::from(at.x) / width, f32::from(at.y) / height)
|
||||
(
|
||||
(f32::from(at.x) / width).clamp(0.0, 1.0),
|
||||
(f32::from(at.y) / height).clamp(0.0, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
impl RenderOnce for InpaintCanvas {
|
||||
|
||||
@@ -203,7 +203,10 @@ impl ModelStatus {
|
||||
|
||||
/// 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}");
|
||||
assert!(total > 0, "GPU memory {used} of none");
|
||||
if used > total {
|
||||
log::error!("model status: GPU memory {used} past {total}; the meter fills");
|
||||
}
|
||||
self.memory = Some((used, total));
|
||||
self
|
||||
}
|
||||
@@ -247,7 +250,7 @@ impl RenderOnce for ModelStatus {
|
||||
Meter::new(
|
||||
(self.id.clone(), "memory"),
|
||||
"GPU memory",
|
||||
used as f32 / total as f32,
|
||||
used.min(total) as f32 / total as f32,
|
||||
)
|
||||
.detail(format!(
|
||||
"{} of {}",
|
||||
|
||||
@@ -38,12 +38,13 @@ impl AspectRatioPicker {
|
||||
selected: (u32, u32),
|
||||
) -> Self {
|
||||
let ratios: Vec<(u32, u32)> = ratios.into_iter().collect();
|
||||
assert!(
|
||||
ratios.contains(&selected),
|
||||
"ratio {}:{} is not among the ratios",
|
||||
selected.0,
|
||||
selected.1
|
||||
);
|
||||
if !ratios.contains(&selected) {
|
||||
log::error!(
|
||||
"aspect ratio: {}:{} is not among the ratios; none selected",
|
||||
selected.0,
|
||||
selected.1
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
ratios,
|
||||
|
||||
+17
-8
@@ -4,7 +4,7 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use gpui::{App, ElementId, Entity, IntoElement, RenderOnce, Window};
|
||||
use gpui::{App, ElementId, Entity, EntityId, IntoElement, RenderOnce, Window};
|
||||
|
||||
use crate::{
|
||||
buttons::IconButton,
|
||||
@@ -43,22 +43,31 @@ impl SeedInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// The seed in `field`; none while it is empty.
|
||||
/// The seed in `field`, read after a `SeedInput` drew it; none while it is empty.
|
||||
pub fn read(field: &Entity<TextInput>, cx: &App) -> Option<u32> {
|
||||
let text = field.read(cx).text();
|
||||
let text = field.read(cx).committed();
|
||||
(!text.is_empty()).then(|| text.parse().expect("a seed field holds digits"))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SeedInput {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let fitted = window.use_keyed_state((self.id.clone(), "fitted"), cx, |_, _| false);
|
||||
if !*fitted.read(cx) {
|
||||
fitted.update(cx, |fitted, _| *fitted = true);
|
||||
let fitted =
|
||||
window.use_keyed_state((self.id.clone(), "fitted"), cx, |_, _| None::<EntityId>);
|
||||
let field_id = self.field.entity_id();
|
||||
if *fitted.read(cx) != Some(field_id) {
|
||||
fitted.update(cx, |fitted, _| *fitted = Some(field_id));
|
||||
self.field.update(cx, |input, cx| {
|
||||
input.set_fit(Rc::new(digits));
|
||||
let text = input.text().to_string();
|
||||
input.set_text(text, cx);
|
||||
let kept = match text.is_empty() || text.parse::<u32>().is_ok() {
|
||||
true => text,
|
||||
false => {
|
||||
log::error!("seed input: {text:?} is no seed; cleared");
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
input.set_fit(Rc::new(digits));
|
||||
input.set_text(kept, cx);
|
||||
input.set_placeholder("Random", cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,10 +39,9 @@ impl StylePresetPicker {
|
||||
) -> Self {
|
||||
let (presets, selected): (Vec<StylePreset>, SharedString) =
|
||||
(presets.into_iter().collect(), selected.into());
|
||||
assert!(
|
||||
presets.iter().any(|preset| preset.key == selected),
|
||||
"style {selected} is not among the presets"
|
||||
);
|
||||
if !presets.iter().any(|preset| preset.key == selected) {
|
||||
log::error!("style presets: {selected} is not among the presets; none selected");
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
presets,
|
||||
|
||||
@@ -8,6 +8,7 @@ mod models;
|
||||
mod results;
|
||||
mod settings;
|
||||
mod sound;
|
||||
mod stale;
|
||||
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{Context, IntoElement, ParentElement, Render, Styled, TestAppContext,
|
||||
use super::{press, settle, setup, tab};
|
||||
use crate::generative::{
|
||||
Download, DownloadState, FineTuneJobCard, HardwareMonitor, ModelCard, ModelDownloadManager,
|
||||
ModelState, ModelStatus, Reading, TunePhase,
|
||||
Reading, TunePhase,
|
||||
};
|
||||
|
||||
type Heard = Rc<RefCell<Vec<String>>>;
|
||||
@@ -120,12 +120,6 @@ fn cards_offer_what_their_state_allows(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[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() {
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, EntityInputHandler as _, Focusable as _, IntoElement,
|
||||
Modifiers, MouseButton, ParentElement, Render, Styled, TestAppContext, Window, div, point, px,
|
||||
};
|
||||
|
||||
use super::{press, settle, setup, tab};
|
||||
use crate::forms::TextInput;
|
||||
use crate::generative::{
|
||||
ABCompareView, AspectRatioPicker, Embedded, EmbeddingVisualizer, FineTuneJobCard,
|
||||
InpaintCanvas, MaskStroke, ModelState, ModelStatus, Outcome, PromptVersionHistory, SeedInput,
|
||||
Shot, StylePreset, StylePresetPicker, TTSVoicePicker, TunePhase, VariationPicker, Verdict,
|
||||
VideoGenerationTimeline, Voice,
|
||||
};
|
||||
|
||||
/// Each picker asked for what its list no longer holds, and counts past their whole.
|
||||
struct Stale;
|
||||
|
||||
impl Render for Stale {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let preset = StylePreset {
|
||||
key: "ink".into(),
|
||||
name: "Ink".into(),
|
||||
picture: None,
|
||||
};
|
||||
let voice = Voice {
|
||||
key: "iris".into(),
|
||||
name: "Iris".into(),
|
||||
about: "a voice".into(),
|
||||
tags: Vec::new(),
|
||||
};
|
||||
let shot = Shot {
|
||||
key: "a".into(),
|
||||
prompt: "a pan".into(),
|
||||
length: Duration::from_secs(4),
|
||||
outcome: Outcome::Done("missing.jpg".into()),
|
||||
};
|
||||
let point = |group| Embedded {
|
||||
key: format!("in-{group}").into(),
|
||||
label: "a point".into(),
|
||||
group,
|
||||
at: [0.0, 0.0, 0.0],
|
||||
};
|
||||
div()
|
||||
.w(px(640.0))
|
||||
.child(
|
||||
ABCompareView::new("compare")
|
||||
.side("Orchid", "one answer")
|
||||
.side("Juniper", "another")
|
||||
.verdict(Verdict::Side(5)),
|
||||
)
|
||||
.child(AspectRatioPicker::new("ratios", [(1, 1), (4, 3)], (16, 9)))
|
||||
.child(StylePresetPicker::new("styles", [preset], "gone"))
|
||||
.child(
|
||||
VideoGenerationTimeline::new("timeline", [shot], Duration::ZERO).selected("gone"),
|
||||
)
|
||||
.child(TTSVoicePicker::new("voices", [voice]).selected("gone"))
|
||||
.child(VariationPicker::new("variations", ["a.jpg"], 1.5, 4))
|
||||
.child(PromptVersionHistory::new("history", [], 0))
|
||||
.child(FineTuneJobCard::new(
|
||||
"tune",
|
||||
"Orchid tuned",
|
||||
"Orchid",
|
||||
"notes.jsonl",
|
||||
TunePhase::Running(7, 3, 0.5),
|
||||
))
|
||||
.child(ModelStatus::new("model", "Orchid", ModelState::Ready).memory(5, 4))
|
||||
.child(div().h(px(320.0)).child(EmbeddingVisualizer::new(
|
||||
"space",
|
||||
[point(0), point(9)],
|
||||
["Docs"],
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn gone_choices_mark_none_and_overruns_peg(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Stale);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
/// A seed field the owner may swap for another.
|
||||
struct Seeded(Entity<TextInput>);
|
||||
|
||||
impl Render for Seeded {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().w(px(420.0)).child(SeedInput::new("seed", &self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_seed_field_fits_whatever_it_held_and_each_new_field(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|window, cx| {
|
||||
let field = cx.new(|cx| TextInput::new(window, cx));
|
||||
field.update(cx, |input, cx| {
|
||||
input.set_text("4294967296", cx);
|
||||
input.insert("7", cx);
|
||||
});
|
||||
Seeded(field)
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(SeedInput::read(&view.0, cx), None)
|
||||
});
|
||||
cx.update(|window, cx| {
|
||||
let field = view.read(cx).0.clone();
|
||||
window.focus(&field.focus_handle(cx), cx);
|
||||
});
|
||||
cx.simulate_keystrokes("secondary-z");
|
||||
settle(cx);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
SeedInput::read(&view.0, cx),
|
||||
None,
|
||||
"undo reaches no unfit text"
|
||||
)
|
||||
});
|
||||
let field = cx.update(|window, cx| cx.new(|cx| TextInput::new(window, cx)));
|
||||
view.update(cx, |view, cx| {
|
||||
view.0 = field.clone();
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
cx.update(|window, cx| window.focus(&field.focus_handle(cx), cx));
|
||||
cx.simulate_input("12a3");
|
||||
settle(cx);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(SeedInput::read(&view.0, cx), Some(123))
|
||||
});
|
||||
cx.update(|window, cx| {
|
||||
field.update(cx, |input, cx| {
|
||||
input.replace_and_mark_text_in_range(None, "ni", None, window, cx)
|
||||
})
|
||||
});
|
||||
settle(cx);
|
||||
assert_eq!(
|
||||
field.read_with(cx, |input, _| input.text().to_string()),
|
||||
"123ni"
|
||||
);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
SeedInput::read(&view.0, cx),
|
||||
Some(123),
|
||||
"a composition is not yet the seed"
|
||||
)
|
||||
});
|
||||
cx.simulate_keystrokes("secondary-z secondary-shift-z");
|
||||
settle(cx);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
SeedInput::read(&view.0, cx),
|
||||
Some(123),
|
||||
"redo brings back what was committed"
|
||||
)
|
||||
});
|
||||
cx.update(|window, cx| {
|
||||
field.update(cx, |input, cx| {
|
||||
input.replace_and_mark_text_in_range(None, "ni", None, window, cx);
|
||||
input.set_text("4294967296", cx);
|
||||
})
|
||||
});
|
||||
settle(cx);
|
||||
view.read_with(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
SeedInput::read(&view.0, cx),
|
||||
Some(123),
|
||||
"a refused text keeps what was committed"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
type Heard = Rc<RefCell<Vec<String>>>;
|
||||
|
||||
/// Variations whose chosen one may leave, and the actions asked.
|
||||
struct Varied {
|
||||
chosen: usize,
|
||||
heard: Heard,
|
||||
}
|
||||
|
||||
impl Render for Varied {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let (vary, upscale) = (self.heard.clone(), self.heard.clone());
|
||||
div().w(px(420.0)).child(
|
||||
VariationPicker::new("variations", ["a.jpg"], 1.5, self.chosen)
|
||||
.on_vary(move |strong, _, _| vary.borrow_mut().push(format!("vary {strong}")))
|
||||
.on_upscale(move |_, _| upscale.borrow_mut().push("upscale".into())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn no_picture_chosen_offers_no_way_on(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let heard = Heard::default();
|
||||
let kept = heard.clone();
|
||||
let (view, cx) = cx.add_window_view(|_, _| Varied {
|
||||
chosen: 0,
|
||||
heard: kept,
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
tab(2, cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(*heard.borrow(), ["vary false"]);
|
||||
heard.borrow_mut().clear();
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = 4;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
cx.update(|window, cx| window.blur(cx));
|
||||
for _ in 0..4 {
|
||||
tab(1, cx);
|
||||
press("enter", cx);
|
||||
}
|
||||
assert_eq!(*heard.borrow(), Vec::<String>::new());
|
||||
}
|
||||
|
||||
/// A canvas the owner narrows, keeping every stroke it hands over.
|
||||
struct Painting {
|
||||
width: f32,
|
||||
strokes: Vec<MaskStroke>,
|
||||
}
|
||||
|
||||
impl Render for Painting {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let owner = cx.entity();
|
||||
div().w(px(self.width)).child(
|
||||
InpaintCanvas::new("canvas", "missing.jpg", 2.0, self.strokes.clone()).on_stroke(
|
||||
move |stroke, _, cx| {
|
||||
owner.update(cx, |view, cx| {
|
||||
view.strokes.push(stroke);
|
||||
cx.notify();
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_canvas_that_shrinks_under_the_pen_ends_the_stroke_on_it(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Painting {
|
||||
width: 400.0,
|
||||
strokes: Vec::new(),
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
let none = Modifiers::none();
|
||||
cx.simulate_mouse_down(point(px(300.0), px(100.0)), MouseButton::Left, none);
|
||||
cx.simulate_mouse_move(point(px(320.0), px(100.0)), MouseButton::Left, none);
|
||||
cx.simulate_mouse_move(point(px(340.0), px(100.0)), MouseButton::Left, none);
|
||||
settle(cx);
|
||||
view.update(cx, |view, cx| {
|
||||
view.width = 200.0;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
cx.simulate_mouse_up(point(px(160.0), px(50.0)), MouseButton::Left, none);
|
||||
settle(cx);
|
||||
let strokes = view.read_with(cx, |view, _| view.strokes.clone());
|
||||
assert_eq!(strokes.len(), 1);
|
||||
let on = |share: f32| (0.0..=1.0).contains(&share);
|
||||
assert!(strokes[0].points.iter().all(|&(x, y)| on(x) && on(y)));
|
||||
}
|
||||
@@ -71,11 +71,16 @@ impl FineTuneJobCard {
|
||||
data: impl Into<SharedString>,
|
||||
phase: TunePhase,
|
||||
) -> Self {
|
||||
if let TunePhase::Running(epoch, epochs, share) = phase {
|
||||
let mut phase = phase;
|
||||
if let TunePhase::Running(epoch, epochs, share) = &mut phase {
|
||||
assert!(
|
||||
(1..=epochs).contains(&epoch) && (0.0..=1.0).contains(&share),
|
||||
*epochs > 0 && (0.0..=1.0).contains(share),
|
||||
"epoch {epoch} of {epochs} at {share}"
|
||||
);
|
||||
if !(1..=*epochs).contains(epoch) {
|
||||
log::error!("fine-tuning: epoch {epoch} of {epochs}; pegged");
|
||||
*epoch = (*epoch).clamp(1, *epochs);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
|
||||
+59
-47
@@ -21,7 +21,7 @@ pub struct VariationPicker {
|
||||
id: ElementId,
|
||||
pictures: Vec<SharedString>,
|
||||
ratio: f32,
|
||||
chosen: usize,
|
||||
chosen: Option<usize>,
|
||||
on_choose: Option<Pick>,
|
||||
on_vary: Option<OnFlag>,
|
||||
on_upscale: Option<Run>,
|
||||
@@ -36,16 +36,18 @@ impl VariationPicker {
|
||||
chosen: usize,
|
||||
) -> Self {
|
||||
let pictures: Vec<SharedString> = pictures.into_iter().map(Into::into).collect();
|
||||
assert!(
|
||||
chosen < pictures.len(),
|
||||
"variation {chosen} of {}",
|
||||
pictures.len()
|
||||
);
|
||||
let held = chosen < pictures.len();
|
||||
if !held {
|
||||
log::error!(
|
||||
"variations: variation {chosen} of {}; none chosen",
|
||||
pictures.len()
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
pictures,
|
||||
ratio: checked_ratio(ratio),
|
||||
chosen,
|
||||
chosen: held.then_some(chosen),
|
||||
on_choose: None,
|
||||
on_vary: None,
|
||||
on_upscale: None,
|
||||
@@ -89,45 +91,50 @@ impl RenderOnce for VariationPicker {
|
||||
let (large, small) = (theme.radius(Radius::Lg), theme.radius(Radius::Md));
|
||||
let thumb = theme.avatar_size(AvatarSize::Xl);
|
||||
let length = motion::duration(motion::BASE, cx);
|
||||
let picture = Image::new(
|
||||
(self.id.clone(), format!("large-{}", self.chosen)),
|
||||
source(&self.pictures[self.chosen]),
|
||||
)
|
||||
.size_full()
|
||||
.rounded(large);
|
||||
let shown = match turn {
|
||||
0 => picture.into_any_element(),
|
||||
_ => div()
|
||||
.size_full()
|
||||
.child(picture)
|
||||
.with_animation(
|
||||
(self.id.clone(), format!("fade-{turn}")),
|
||||
Animation::new(length).with_easing(motion::ease_out_cubic),
|
||||
|shown, t| shown.opacity(0.4 + 0.6 * t),
|
||||
)
|
||||
.into_any_element(),
|
||||
};
|
||||
let shown = self.chosen.map(|chosen| {
|
||||
let picture = Image::new(
|
||||
(self.id.clone(), format!("large-{chosen}")),
|
||||
source(&self.pictures[chosen]),
|
||||
)
|
||||
.size_full()
|
||||
.rounded(large);
|
||||
match turn {
|
||||
0 => picture.into_any_element(),
|
||||
_ => div()
|
||||
.size_full()
|
||||
.child(picture)
|
||||
.with_animation(
|
||||
(self.id.clone(), format!("fade-{turn}")),
|
||||
Animation::new(length).with_easing(motion::ease_out_cubic),
|
||||
|shown, t| shown.opacity(0.4 + 0.6 * t),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
});
|
||||
let vary = |strong: bool, label: &'static str| {
|
||||
self.on_vary.clone().map(|vary| {
|
||||
Button::new((self.id.clone(), label), label)
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.size(ControlSize::Sm)
|
||||
.icon(IconName::Shuffle)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("variation picker: vary, strong {strong}");
|
||||
vary(strong, window, cx)
|
||||
})
|
||||
})
|
||||
self.on_vary
|
||||
.clone()
|
||||
.filter(|_| self.chosen.is_some())
|
||||
.map(|vary| {
|
||||
Button::new((self.id.clone(), label), label)
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.size(ControlSize::Sm)
|
||||
.icon(IconName::Shuffle)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("variation picker: vary, strong {strong}");
|
||||
vary(strong, window, cx)
|
||||
})
|
||||
})
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(framed(self.ratio, cx).rounded(large).child(shown))
|
||||
.child(framed(self.ratio, cx).rounded(large).children(shown))
|
||||
.child(div().flex().flex_wrap().gap_2().children(
|
||||
self.pictures.iter().zip(thumbs).enumerate().map(
|
||||
|(ix, (picture, (focus, focused)))| {
|
||||
let chosen = ix == self.chosen;
|
||||
let chosen = self.chosen == Some(ix);
|
||||
let choose = self.on_choose.clone();
|
||||
div()
|
||||
.id((self.id.clone(), format!("thumb-{ix}")))
|
||||
@@ -176,16 +183,21 @@ impl RenderOnce for VariationPicker {
|
||||
.gap_2()
|
||||
.children(vary(false, "Vary subtly"))
|
||||
.children(vary(true, "Vary strongly"))
|
||||
.children(self.on_upscale.clone().map(|upscale| {
|
||||
Button::new((self.id.clone(), "upscale"), "Upscale")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.size(ControlSize::Sm)
|
||||
.icon(IconName::Maximize2)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("variation picker: upscale");
|
||||
upscale(window, cx)
|
||||
})
|
||||
})),
|
||||
.children(
|
||||
self.on_upscale
|
||||
.clone()
|
||||
.filter(|_| self.chosen.is_some())
|
||||
.map(|upscale| {
|
||||
Button::new((self.id.clone(), "upscale"), "Upscale")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.size(ControlSize::Sm)
|
||||
.icon(IconName::Maximize2)
|
||||
.on_click(move |_, window, cx| {
|
||||
log::info!("variation picker: upscale");
|
||||
upscale(window, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+23
-16
@@ -118,7 +118,7 @@ pub struct PromptVersion {
|
||||
pub struct PromptVersionHistory {
|
||||
id: ElementId,
|
||||
versions: Vec<PromptVersion>,
|
||||
chosen: usize,
|
||||
chosen: Option<usize>,
|
||||
on_choose: Option<Pick>,
|
||||
on_restore: Option<Pick>,
|
||||
}
|
||||
@@ -131,15 +131,17 @@ impl PromptVersionHistory {
|
||||
chosen: usize,
|
||||
) -> Self {
|
||||
let versions: Vec<PromptVersion> = versions.into_iter().collect();
|
||||
assert!(
|
||||
chosen < versions.len(),
|
||||
"version {chosen} of {}",
|
||||
versions.len()
|
||||
);
|
||||
let held = chosen < versions.len();
|
||||
if !held {
|
||||
log::error!(
|
||||
"prompt versions: version {chosen} of {}; none chosen",
|
||||
versions.len()
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
versions,
|
||||
chosen,
|
||||
chosen: held.then_some(chosen),
|
||||
on_choose: None,
|
||||
on_restore: None,
|
||||
}
|
||||
@@ -175,13 +177,18 @@ impl RenderOnce for PromptVersionHistory {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let chosen = self.chosen;
|
||||
let (heading, before) = compared(&self.versions, chosen);
|
||||
let shown = &self.versions[chosen];
|
||||
let restore = self
|
||||
.on_restore
|
||||
.clone()
|
||||
.filter(|_| chosen > 0)
|
||||
.map(|restore| {
|
||||
let diff = chosen.map(|chosen| {
|
||||
let (heading, before) = compared(&self.versions, chosen);
|
||||
(heading, before, self.versions[chosen].text.clone())
|
||||
});
|
||||
let heading: SharedString = match &diff {
|
||||
Some((heading, ..)) => heading.clone(),
|
||||
None => "No version is chosen".into(),
|
||||
};
|
||||
let restore = chosen
|
||||
.filter(|chosen| *chosen > 0)
|
||||
.zip(self.on_restore.clone())
|
||||
.map(|(chosen, restore)| {
|
||||
Button::new((self.id.clone(), "restore"), "Restore")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.size(ControlSize::Sm)
|
||||
@@ -192,7 +199,7 @@ impl RenderOnce for PromptVersionHistory {
|
||||
});
|
||||
let rows = self.versions.iter().zip(focuses).enumerate().map(
|
||||
|(ix, (version, (focus, focused)))| {
|
||||
let on = ix == chosen;
|
||||
let on = chosen == Some(ix);
|
||||
let choose = self.on_choose.clone();
|
||||
div()
|
||||
.id((self.id.clone(), format!("version-{ix}")))
|
||||
@@ -291,7 +298,7 @@ impl RenderOnce for PromptVersionHistory {
|
||||
)
|
||||
.children(restore),
|
||||
)
|
||||
.child(PromptDiff::new(before, shown.text.clone())),
|
||||
.children(diff.map(|(_, before, text)| PromptDiff::new(before, text))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@ impl VideoGenerationTimeline {
|
||||
|
||||
impl RenderOnce for VideoGenerationTimeline {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let named = (self.selected.as_ref())
|
||||
.is_none_or(|key| self.shots.iter().any(|shot| &shot.key == key));
|
||||
assert!(named, "the chosen shot is not listed");
|
||||
if let Some(key) = &self.selected
|
||||
&& !self.shots.iter().any(|shot| &shot.key == key)
|
||||
{
|
||||
log::error!("video timeline: shot {key} is not listed; none selected");
|
||||
}
|
||||
let pickable = self.on_select.is_some();
|
||||
let scroll = window
|
||||
.use_keyed_state((self.id.clone(), "scroll"), cx, |_, _| ScrollHandle::new())
|
||||
|
||||
@@ -80,14 +80,11 @@ impl TTSVoicePicker {
|
||||
|
||||
impl RenderOnce for TTSVoicePicker {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let known = |key: &Option<SharedString>| {
|
||||
key.as_ref()
|
||||
.is_none_or(|key| self.voices.iter().any(|voice| &voice.key == key))
|
||||
};
|
||||
assert!(
|
||||
known(&self.selected) && known(&self.playing),
|
||||
"a voice named is not listed"
|
||||
);
|
||||
for key in [&self.selected, &self.playing].into_iter().flatten() {
|
||||
if !self.voices.iter().any(|voice| &voice.key == key) {
|
||||
log::error!("voice picker: voice {key} is not listed; none marked");
|
||||
}
|
||||
}
|
||||
let pickable = self.on_select.is_some();
|
||||
let focuses: Vec<_> = self
|
||||
.voices
|
||||
|
||||
+2
-1
@@ -39,7 +39,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
||||
- [x] P22 Collaboration — `src/collab`
|
||||
- [x] P23 AI Chat — `src/chat`
|
||||
- [x] P24 Agent — `src/agent`
|
||||
- [ ] P25 Generative — `src/generative`
|
||||
- [x] P25 Generative — `src/generative`
|
||||
- [ ] P26 Media — `src/media`
|
||||
- [ ] P27 Files — `src/files`
|
||||
- [ ] P28 Messaging — `src/messaging`
|
||||
@@ -90,3 +90,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- P22: annotations and suggestions outside their text log and stay out, picks and decisions keeping the owner's index; `accept` returns `Option`; a gone pick, active thread or followed peer logs; yours at no count comes off; a masked field clamps a stale offset, so remote cursors and selections draw. Kept: a thread's comments, presence max, one list's order, empty marks, constant role and mode tables. Review: 3 rounds.
|
||||
- P23: a branch version past its total logs and shows a dash, its arrows off; a parameter outside its range logs and pegs; a passage's matches outside it log and stay out. Kept: one-value contracts (progress, scores, shapes, peaks, levels, limits, prices, capacity, format, hour), the composer's trigger rows and stream reveal by construction; `places` skips zero, so `decimals` meets no zero step. Review: 2 rounds.
|
||||
- P24: a checkpoint or browser frame past its list logs and marks none; progress past its total and memory past its limit log and fill their bars, the text keeping the real counts; three `should_panic` tests became render tests. Kept: the artifact's preview-or-source builder check, one-value contracts (cost, cpu, pointer, totals and limits above zero), registry lookups over a constant table. Review: 1 round.
|
||||
- P25: a verdict, ratio, style, shot, voice, variation or prompt version gone from its list logs and marks none (no picture, no Vary or Upscale, no diff); embedding points in a gone group stay out; an epoch and GPU memory past their whole log and peg. A seed field holds a seed: each field handed in is fitted, and text that is no seed clears; in `TextInput`, a fit starts a fresh undo history, undo keeps and `set_text` fits against the committed text, never an input method's unfinished one (`committed`), and a lifted mask stroke stays on its picture. Kept: one-value contracts, unique group names, one snapshot's loss lists, two to four sides, the legend's lookup. Review: 5 rounds, the cap; round 5's finding (`set_text` over a composition) is fixed and tested, and P26's review checks it.
|
||||
Reference in new issue
Block a user