1use std::fmt;
4use std::rc::Rc;
5
6use ravenrs_app_i18n::{Language, Texts};
7use ravenrs_layout::Align;
8use ravenrs_style::{Semantic, TextStyle};
9
10use crate::context::{BuildCx, NodeId};
11use crate::view::{AnyView, View};
12use crate::{
13 Alert, Button, ButtonSize, ButtonStyle, Card, HStack, IconName, ProgressBar, Select, Text, Tone, VStack,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
18#[non_exhaustive]
19#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
20pub enum ModelStatus {
21 Available,
23 Downloading(f32),
25 Installed,
27 Testing,
29}
30
31#[derive(Clone, Debug, PartialEq)]
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
34pub struct ModelChoice {
35 pub name: String,
37 pub details: String,
39 pub status: ModelStatus,
41 pub in_use: bool,
43 pub result: Option<String>,
45 pub error: Option<String>,
47}
48
49#[derive(Clone, Debug)]
51#[non_exhaustive]
52#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
53pub struct ModelPickerLabels {
54 pub download: String,
56 pub cancel: String,
58 pub test: String,
60 pub testing: String,
62 pub use_it: String,
64 pub stop_using: String,
66 pub in_use: String,
68 pub remove: String,
70 pub nothing_fits: String,
72 pub model: String,
74 pub downloaded: String,
76}
77
78impl Default for ModelPickerLabels {
79 fn default() -> Self {
80 Self::from_texts(&Texts::new(Language::English))
81 }
82}
83
84#[cfg_attr(feature = "live", ravenrs_live::expose)]
85impl ModelPickerLabels {
86 pub fn from_texts(texts: &Texts) -> Self {
88 Self {
89 download: texts.get("widgets.model_picker.download"),
90 cancel: texts.get("widgets.model_picker.cancel"),
91 test: texts.get("widgets.model_picker.test"),
92 testing: texts.get("widgets.model_picker.testing"),
93 use_it: texts.get("widgets.model_picker.use_it"),
94 stop_using: texts.get("widgets.model_picker.stop_using"),
95 in_use: texts.get("widgets.model_picker.in_use"),
96 remove: texts.get("widgets.model_picker.remove"),
97 nothing_fits: texts.get("widgets.model_picker.nothing_fits"),
98 model: texts.get("widgets.model_picker.model"),
99 downloaded: texts.get("widgets.model_picker.downloaded"),
100 }
101 }
102
103 pub fn pt_br() -> Self {
105 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
106 }
107}
108
109type OnChoice = Rc<dyn Fn(usize)>;
110
111#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
129pub struct ModelPicker {
130 choices: Vec<ModelChoice>,
131 labels: ModelPickerLabels,
132 on_download: Option<OnChoice>,
133 on_cancel: Option<OnChoice>,
134 on_test: Option<OnChoice>,
135 on_use: Option<OnChoice>,
136 on_remove: Option<OnChoice>,
137}
138
139impl fmt::Debug for ModelPicker {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.debug_struct("ModelPicker").field("choices", &self.choices).finish_non_exhaustive()
142 }
143}
144
145#[cfg_attr(feature = "live", ravenrs_live::expose)]
146impl ModelPicker {
147 pub fn new(choices: Vec<ModelChoice>) -> Self {
149 Self {
150 choices,
151 labels: ModelPickerLabels::default(),
152 on_download: None,
153 on_cancel: None,
154 on_test: None,
155 on_use: None,
156 on_remove: None,
157 }
158 }
159
160 #[must_use]
162 pub fn labels(mut self, labels: ModelPickerLabels) -> Self {
163 self.labels = labels;
164 self
165 }
166
167 #[must_use]
169 pub fn on_download(mut self, handler: impl Fn(usize) + 'static) -> Self {
170 self.on_download = Some(Rc::new(handler));
171 self
172 }
173
174 #[must_use]
176 pub fn on_cancel(mut self, handler: impl Fn(usize) + 'static) -> Self {
177 self.on_cancel = Some(Rc::new(handler));
178 self
179 }
180
181 #[must_use]
183 pub fn on_test(mut self, handler: impl Fn(usize) + 'static) -> Self {
184 self.on_test = Some(Rc::new(handler));
185 self
186 }
187
188 #[must_use]
190 pub fn on_use(mut self, handler: impl Fn(usize) + 'static) -> Self {
191 self.on_use = Some(Rc::new(handler));
192 self
193 }
194
195 #[must_use]
197 pub fn on_remove(mut self, handler: impl Fn(usize) + 'static) -> Self {
198 self.on_remove = Some(Rc::new(handler));
199 self
200 }
201
202 fn button(
203 label: &str,
204 style: ButtonStyle,
205 icon: IconName,
206 index: usize,
207 handler: Option<&OnChoice>,
208 ) -> Button {
209 let handler = handler.cloned();
210 Button::new(label.to_owned()).style(style).size(ButtonSize::Small).icon(icon).on_click(move || {
211 if let Some(handler) = &handler {
212 handler(index);
213 }
214 })
215 }
216
217 fn actions(&self, index: usize, choice: &ModelChoice) -> AnyView {
218 let labels = &self.labels;
219 match choice.status {
220 ModelStatus::Available => Box::new(Self::button(
221 &labels.download,
222 ButtonStyle::Primary,
223 IconName::Download,
224 index,
225 self.on_download.as_ref(),
226 )),
227 ModelStatus::Downloading(_) => Box::new(Self::button(
228 &labels.cancel,
229 ButtonStyle::Secondary,
230 IconName::X,
231 index,
232 self.on_cancel.as_ref(),
233 )),
234 ModelStatus::Testing => Box::new(
235 Text::new(labels.testing.clone()).style(TextStyle::Callout).foreground(Semantic::InkMuted),
236 ),
237 _ => Box::new(
238 HStack::new((
239 self.on_test.as_ref().map(|handler| {
240 Self::button(
241 &labels.test,
242 ButtonStyle::Secondary,
243 IconName::Zap,
244 index,
245 Some(handler),
246 )
247 }),
248 self.on_use.as_ref().map(|handler| {
249 Self::button(
250 if choice.in_use { &labels.stop_using } else { &labels.use_it },
251 if choice.in_use { ButtonStyle::Secondary } else { ButtonStyle::Primary },
252 IconName::Sparkles,
253 index,
254 Some(handler),
255 )
256 }),
257 Self::button(
258 &labels.remove,
259 ButtonStyle::Ghost,
260 IconName::Trash2,
261 index,
262 self.on_remove.as_ref(),
263 ),
264 ))
265 .spacing(8.0)
266 .wrap(),
267 ),
268 }
269 }
270}
271
272#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
273impl ModelPicker {
274 fn option(&self, choice: &ModelChoice) -> String {
276 let labels = &self.labels;
277 match choice.status {
278 _ if choice.in_use => format!("{} · {}", choice.name, labels.in_use),
279 ModelStatus::Downloading(fraction) => {
280 format!("{} · {:.0}%", choice.name, fraction.clamp(0.0, 1.0) * 100.0)
281 }
282 ModelStatus::Installed | ModelStatus::Testing => {
283 format!("{} · {}", choice.name, labels.downloaded)
284 }
285 _ => choice.name.clone(),
286 }
287 }
288}
289
290impl View for ModelPicker {
291 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
292 if self.choices.is_empty() {
293 let text = Text::new(self.labels.nothing_fits.clone())
294 .style(TextStyle::Callout)
295 .foreground(Semantic::InkMuted);
296 return cx.child(&text);
297 }
298 let selected = cx.use_state(|| {
300 let installed = |choice: &ModelChoice| choice.status != ModelStatus::Available;
301 self.choices
302 .iter()
303 .position(|choice| choice.in_use)
304 .or_else(|| self.choices.iter().position(installed))
305 .unwrap_or(0)
306 });
307 let index = selected.get().min(self.choices.len() - 1);
308 let choice = &self.choices[index];
309 let options: Vec<String> = self.choices.iter().map(|choice| self.option(choice)).collect();
310 let progress = match choice.status {
311 ModelStatus::Downloading(fraction) => Some(ProgressBar::new(fraction)),
312 _ => None,
313 };
314 let card = Card::new(
315 VStack::new((
316 Select::new(selected.binding(), options).label(self.labels.model.clone()),
317 Text::new(choice.details.clone()).style(TextStyle::Callout).foreground(Semantic::InkMuted),
318 progress,
319 choice.result.clone().map(|result| Text::new(result).style(TextStyle::Callout)),
320 choice.error.clone().map(|error| Alert::new("", error).tone(Tone::Danger)),
321 self.actions(index, choice),
322 ))
323 .spacing(10.0)
324 .align(Align::Stretch),
325 );
326 cx.child(&card)
327 }
328}