1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
6 Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::collections::HashMap;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum ModelStatus {
16 #[default]
18 Draft,
19 Review,
21 Published,
23 Deprecated,
25 Archived,
27}
28
29impl ModelStatus {
30 #[must_use]
32 pub fn color(&self) -> Color {
33 match self {
34 Self::Draft => Color::new(0.6, 0.6, 0.6, 1.0),
35 Self::Review => Color::new(0.9, 0.7, 0.1, 1.0),
36 Self::Published => Color::new(0.2, 0.7, 0.3, 1.0),
37 Self::Deprecated => Color::new(0.9, 0.5, 0.1, 1.0),
38 Self::Archived => Color::new(0.5, 0.5, 0.5, 1.0),
39 }
40 }
41
42 #[must_use]
44 pub const fn label(&self) -> &'static str {
45 match self {
46 Self::Draft => "Draft",
47 Self::Review => "Review",
48 Self::Published => "Published",
49 Self::Deprecated => "Deprecated",
50 Self::Archived => "Archived",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct ModelMetric {
58 pub name: String,
60 pub value: f64,
62 pub unit: Option<String>,
64 pub higher_is_better: bool,
66}
67
68impl ModelMetric {
69 #[must_use]
71 pub fn new(name: impl Into<String>, value: f64) -> Self {
72 Self {
73 name: name.into(),
74 value,
75 unit: None,
76 higher_is_better: true,
77 }
78 }
79
80 #[must_use]
82 pub fn unit(mut self, unit: impl Into<String>) -> Self {
83 self.unit = Some(unit.into());
84 self
85 }
86
87 #[must_use]
89 pub const fn lower_is_better(mut self) -> Self {
90 self.higher_is_better = false;
91 self
92 }
93
94 #[must_use]
96 pub fn formatted_value(&self) -> String {
97 if let Some(ref unit) = self.unit {
98 format!("{:.2}{}", self.value, unit)
99 } else if self.value.abs() < 1.0 {
100 format!("{:.2}%", self.value * 100.0)
101 } else {
102 format!("{:.2}", self.value)
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ModelCard {
110 name: String,
112 version: String,
114 description: Option<String>,
116 status: ModelStatus,
118 framework: Option<String>,
120 task: Option<String>,
122 metrics: Vec<ModelMetric>,
124 parameters: Option<u64>,
126 dataset: Option<String>,
128 author: Option<String>,
130 tags: Vec<String>,
132 metadata: HashMap<String, String>,
134 width: Option<f32>,
136 height: Option<f32>,
138 background: Color,
140 border_color: Color,
142 corner_radius: f32,
144 show_metrics_chart: bool,
146 accessible_name_value: Option<String>,
148 test_id_value: Option<String>,
150 #[serde(skip)]
152 bounds: Rect,
153}
154
155impl Default for ModelCard {
156 fn default() -> Self {
157 Self {
158 name: String::new(),
159 version: String::from("1.0.0"),
160 description: None,
161 status: ModelStatus::Draft,
162 framework: None,
163 task: None,
164 metrics: Vec::new(),
165 parameters: None,
166 dataset: None,
167 author: None,
168 tags: Vec::new(),
169 metadata: HashMap::new(),
170 width: None,
171 height: None,
172 background: Color::WHITE,
173 border_color: Color::new(0.9, 0.9, 0.9, 1.0),
174 corner_radius: 8.0,
175 show_metrics_chart: true,
176 accessible_name_value: None,
177 test_id_value: None,
178 bounds: Rect::default(),
179 }
180 }
181}
182
183impl ModelCard {
184 #[must_use]
186 pub fn new(name: impl Into<String>) -> Self {
187 Self {
188 name: name.into(),
189 ..Self::default()
190 }
191 }
192
193 #[must_use]
195 pub fn name(mut self, name: impl Into<String>) -> Self {
196 self.name = name.into();
197 self
198 }
199
200 #[must_use]
202 pub fn version(mut self, version: impl Into<String>) -> Self {
203 self.version = version.into();
204 self
205 }
206
207 #[must_use]
209 pub fn description(mut self, desc: impl Into<String>) -> Self {
210 self.description = Some(desc.into());
211 self
212 }
213
214 #[must_use]
216 pub const fn status(mut self, status: ModelStatus) -> Self {
217 self.status = status;
218 self
219 }
220
221 #[must_use]
223 pub fn framework(mut self, framework: impl Into<String>) -> Self {
224 self.framework = Some(framework.into());
225 self
226 }
227
228 #[must_use]
230 pub fn task(mut self, task: impl Into<String>) -> Self {
231 self.task = Some(task.into());
232 self
233 }
234
235 #[must_use]
237 pub fn metric(mut self, metric: ModelMetric) -> Self {
238 self.metrics.push(metric);
239 self
240 }
241
242 #[must_use]
244 pub fn metrics(mut self, metrics: impl IntoIterator<Item = ModelMetric>) -> Self {
245 self.metrics.extend(metrics);
246 self
247 }
248
249 #[must_use]
251 pub const fn parameters(mut self, count: u64) -> Self {
252 self.parameters = Some(count);
253 self
254 }
255
256 #[must_use]
258 pub fn dataset(mut self, dataset: impl Into<String>) -> Self {
259 self.dataset = Some(dataset.into());
260 self
261 }
262
263 #[must_use]
265 pub fn author(mut self, author: impl Into<String>) -> Self {
266 self.author = Some(author.into());
267 self
268 }
269
270 #[must_use]
272 pub fn tag(mut self, tag: impl Into<String>) -> Self {
273 self.tags.push(tag.into());
274 self
275 }
276
277 #[must_use]
279 pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
280 self.tags.extend(tags.into_iter().map(Into::into));
281 self
282 }
283
284 #[must_use]
286 pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
287 self.metadata.insert(key.into(), value.into());
288 self
289 }
290
291 #[must_use]
293 pub fn width(mut self, width: f32) -> Self {
294 self.width = Some(width.max(200.0));
295 self
296 }
297
298 #[must_use]
300 pub fn height(mut self, height: f32) -> Self {
301 self.height = Some(height.max(150.0));
302 self
303 }
304
305 #[must_use]
307 pub const fn background(mut self, color: Color) -> Self {
308 self.background = color;
309 self
310 }
311
312 #[must_use]
314 pub const fn border_color(mut self, color: Color) -> Self {
315 self.border_color = color;
316 self
317 }
318
319 #[must_use]
321 pub fn corner_radius(mut self, radius: f32) -> Self {
322 self.corner_radius = radius.max(0.0);
323 self
324 }
325
326 #[must_use]
328 pub const fn show_metrics_chart(mut self, show: bool) -> Self {
329 self.show_metrics_chart = show;
330 self
331 }
332
333 #[must_use]
335 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
336 self.accessible_name_value = Some(name.into());
337 self
338 }
339
340 #[must_use]
342 pub fn test_id(mut self, id: impl Into<String>) -> Self {
343 self.test_id_value = Some(id.into());
344 self
345 }
346
347 #[must_use]
351 pub fn get_name(&self) -> &str {
352 &self.name
353 }
354
355 #[must_use]
357 pub fn get_version(&self) -> &str {
358 &self.version
359 }
360
361 #[must_use]
363 pub fn get_description(&self) -> Option<&str> {
364 self.description.as_deref()
365 }
366
367 #[must_use]
369 pub const fn get_status(&self) -> ModelStatus {
370 self.status
371 }
372
373 #[must_use]
375 pub fn get_framework(&self) -> Option<&str> {
376 self.framework.as_deref()
377 }
378
379 #[must_use]
381 pub fn get_task(&self) -> Option<&str> {
382 self.task.as_deref()
383 }
384
385 #[must_use]
387 pub fn get_metrics(&self) -> &[ModelMetric] {
388 &self.metrics
389 }
390
391 #[must_use]
393 pub const fn get_parameters(&self) -> Option<u64> {
394 self.parameters
395 }
396
397 #[must_use]
399 pub fn get_dataset(&self) -> Option<&str> {
400 self.dataset.as_deref()
401 }
402
403 #[must_use]
405 pub fn get_author(&self) -> Option<&str> {
406 self.author.as_deref()
407 }
408
409 #[must_use]
411 pub fn get_tags(&self) -> &[String] {
412 &self.tags
413 }
414
415 #[must_use]
417 pub fn get_metadata(&self, key: &str) -> Option<&str> {
418 self.metadata.get(key).map(String::as_str)
419 }
420
421 #[must_use]
423 pub fn has_metrics(&self) -> bool {
424 !self.metrics.is_empty()
425 }
426
427 #[must_use]
429 pub fn formatted_parameters(&self) -> Option<String> {
430 self.parameters.map(|p| {
431 if p >= 1_000_000_000 {
432 format!("{:.1}B", p as f64 / 1_000_000_000.0)
433 } else if p >= 1_000_000 {
434 format!("{:.1}M", p as f64 / 1_000_000.0)
435 } else if p >= 1_000 {
436 format!("{:.1}K", p as f64 / 1_000.0)
437 } else {
438 format!("{p}")
439 }
440 })
441 }
442}
443
444impl Widget for ModelCard {
445 fn type_id(&self) -> TypeId {
446 TypeId::of::<Self>()
447 }
448
449 fn measure(&self, constraints: Constraints) -> Size {
450 let width = self.width.unwrap_or(320.0);
451 let height = self.height.unwrap_or(200.0);
452 constraints.constrain(Size::new(width, height))
453 }
454
455 fn layout(&mut self, bounds: Rect) -> LayoutResult {
456 self.bounds = bounds;
457 LayoutResult {
458 size: bounds.size(),
459 }
460 }
461
462 #[allow(clippy::too_many_lines)]
463 fn paint(&self, canvas: &mut dyn Canvas) {
464 let padding = 16.0;
465
466 canvas.fill_rect(self.bounds, self.background);
468
469 canvas.stroke_rect(self.bounds, self.border_color, 1.0);
471
472 let status_color = self.status.color();
474 let badge_rect = Rect::new(
475 self.bounds.x + self.bounds.width - 80.0,
476 self.bounds.y + padding,
477 70.0,
478 22.0,
479 );
480 canvas.fill_rect(badge_rect, status_color);
481
482 let badge_style = TextStyle {
483 size: 10.0,
484 color: Color::WHITE,
485 ..TextStyle::default()
486 };
487 canvas.draw_text(
488 self.status.label(),
489 Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
490 &badge_style,
491 );
492
493 let title_style = TextStyle {
495 size: 18.0,
496 color: Color::new(0.1, 0.1, 0.1, 1.0),
497 ..TextStyle::default()
498 };
499 canvas.draw_text(
500 &self.name,
501 Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
502 &title_style,
503 );
504
505 let version_style = TextStyle {
507 size: 12.0,
508 color: Color::new(0.5, 0.5, 0.5, 1.0),
509 ..TextStyle::default()
510 };
511 canvas.draw_text(
512 &format!("v{}", self.version),
513 Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
514 &version_style,
515 );
516
517 let mut y_offset = padding + 50.0;
519 if let Some(ref desc) = self.description {
520 let desc_style = TextStyle {
521 size: 12.0,
522 color: Color::new(0.3, 0.3, 0.3, 1.0),
523 ..TextStyle::default()
524 };
525 canvas.draw_text(
526 desc,
527 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
528 &desc_style,
529 );
530 y_offset += 24.0;
531 }
532
533 if self.framework.is_some() || self.task.is_some() {
535 let info_style = TextStyle {
536 size: 11.0,
537 color: Color::new(0.4, 0.4, 0.4, 1.0),
538 ..TextStyle::default()
539 };
540 let info_text = match (&self.framework, &self.task) {
541 (Some(f), Some(t)) => format!("{f} • {t}"),
542 (Some(f), None) => f.clone(),
543 (None, Some(t)) => t.clone(),
544 (None, None) => String::new(),
545 };
546 canvas.draw_text(
547 &info_text,
548 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
549 &info_style,
550 );
551 y_offset += 20.0;
552 }
553
554 if let Some(params) = self.formatted_parameters() {
556 let params_style = TextStyle {
557 size: 11.0,
558 color: Color::new(0.4, 0.4, 0.4, 1.0),
559 ..TextStyle::default()
560 };
561 canvas.draw_text(
562 &format!("Parameters: {params}"),
563 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
564 ¶ms_style,
565 );
566 y_offset += 18.0;
567 }
568
569 if self.show_metrics_chart && !self.metrics.is_empty() {
571 let metric_style = TextStyle {
572 size: 11.0,
573 color: Color::new(0.2, 0.2, 0.2, 1.0),
574 ..TextStyle::default()
575 };
576 let value_style = TextStyle {
577 size: 14.0,
578 color: Color::new(0.2, 0.47, 0.96, 1.0),
579 ..TextStyle::default()
580 };
581
582 let metric_width =
583 (self.bounds.width - padding * 2.0) / self.metrics.len().min(4) as f32;
584 for (i, metric) in self.metrics.iter().take(4).enumerate() {
585 let mx = (i as f32).mul_add(metric_width, self.bounds.x + padding);
586 canvas.draw_text(
587 &metric.name,
588 Point::new(mx, self.bounds.y + y_offset + 12.0),
589 &metric_style,
590 );
591 canvas.draw_text(
592 &metric.formatted_value(),
593 Point::new(mx, self.bounds.y + y_offset + 28.0),
594 &value_style,
595 );
596 }
597 y_offset += 40.0;
598 }
599
600 if !self.tags.is_empty() {
602 let tag_style = TextStyle {
603 size: 10.0,
604 color: Color::new(0.3, 0.3, 0.3, 1.0),
605 ..TextStyle::default()
606 };
607 let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
608
609 let mut tx = self.bounds.x + padding;
610 for tag in self.tags.iter().take(5) {
611 let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
612 canvas.fill_rect(
613 Rect::new(tx, self.bounds.y + y_offset, tag_width, 18.0),
614 tag_bg,
615 );
616 canvas.draw_text(
617 tag,
618 Point::new(tx + 6.0, self.bounds.y + y_offset + 13.0),
619 &tag_style,
620 );
621 tx += tag_width + 6.0;
622 }
623 }
624 }
625
626 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
627 None
628 }
629
630 fn children(&self) -> &[Box<dyn Widget>] {
631 &[]
632 }
633
634 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
635 &mut []
636 }
637
638 fn is_interactive(&self) -> bool {
639 false
640 }
641
642 fn is_focusable(&self) -> bool {
643 false
644 }
645
646 fn accessible_name(&self) -> Option<&str> {
647 self.accessible_name_value.as_deref().or(Some(&self.name))
648 }
649
650 fn accessible_role(&self) -> AccessibleRole {
651 AccessibleRole::Generic
652 }
653
654 fn test_id(&self) -> Option<&str> {
655 self.test_id_value.as_deref()
656 }
657}
658
659impl Brick for ModelCard {
661 fn brick_name(&self) -> &'static str {
662 "ModelCard"
663 }
664
665 fn assertions(&self) -> &[BrickAssertion] {
666 &[BrickAssertion::MaxLatencyMs(16)]
667 }
668
669 fn budget(&self) -> BrickBudget {
670 BrickBudget::uniform(16)
671 }
672
673 fn verify(&self) -> BrickVerification {
674 BrickVerification {
675 passed: self.assertions().to_vec(),
676 failed: vec![],
677 verification_time: Duration::from_micros(10),
678 }
679 }
680
681 fn to_html(&self) -> String {
682 r#"<div class="brick-modelcard"></div>"#.to_string()
683 }
684
685 fn to_css(&self) -> String {
686 ".brick-modelcard { display: block; }".to_string()
687 }
688
689 fn test_id(&self) -> Option<&str> {
690 self.test_id_value.as_deref()
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
701 fn test_model_status_default() {
702 assert_eq!(ModelStatus::default(), ModelStatus::Draft);
703 }
704
705 #[test]
706 fn test_model_status_color() {
707 let published = ModelStatus::Published;
708 let color = published.color();
709 assert!(color.g > color.r); }
711
712 #[test]
713 fn test_model_status_label() {
714 assert_eq!(ModelStatus::Draft.label(), "Draft");
715 assert_eq!(ModelStatus::Review.label(), "Review");
716 assert_eq!(ModelStatus::Published.label(), "Published");
717 assert_eq!(ModelStatus::Deprecated.label(), "Deprecated");
718 assert_eq!(ModelStatus::Archived.label(), "Archived");
719 }
720
721 #[test]
724 fn test_model_metric_new() {
725 let metric = ModelMetric::new("Accuracy", 0.95);
726 assert_eq!(metric.name, "Accuracy");
727 assert_eq!(metric.value, 0.95);
728 assert!(metric.unit.is_none());
729 assert!(metric.higher_is_better);
730 }
731
732 #[test]
733 fn test_model_metric_unit() {
734 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
735 assert_eq!(metric.unit, Some("ms".to_string()));
736 }
737
738 #[test]
739 fn test_model_metric_lower_is_better() {
740 let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
741 assert!(!metric.higher_is_better);
742 }
743
744 #[test]
745 fn test_model_metric_formatted_value_percentage() {
746 let metric = ModelMetric::new("Accuracy", 0.95);
747 assert_eq!(metric.formatted_value(), "95.00%");
748 }
749
750 #[test]
751 fn test_model_metric_formatted_value_with_unit() {
752 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
753 assert_eq!(metric.formatted_value(), "45.00ms");
754 }
755
756 #[test]
757 fn test_model_metric_formatted_value_large() {
758 let metric = ModelMetric::new("Score", 1234.5);
759 assert_eq!(metric.formatted_value(), "1234.50");
760 }
761
762 #[test]
765 fn test_model_card_new() {
766 let card = ModelCard::new("GPT-4");
767 assert_eq!(card.get_name(), "GPT-4");
768 assert_eq!(card.get_version(), "1.0.0");
769 assert_eq!(card.get_status(), ModelStatus::Draft);
770 }
771
772 #[test]
773 fn test_model_card_default() {
774 let card = ModelCard::default();
775 assert!(card.name.is_empty());
776 assert_eq!(card.version, "1.0.0");
777 }
778
779 #[test]
780 fn test_model_card_builder() {
781 let card = ModelCard::new("ResNet-50")
782 .version("2.1.0")
783 .description("Image classification model")
784 .status(ModelStatus::Published)
785 .framework("PyTorch")
786 .task("classification")
787 .metric(ModelMetric::new("Top-1 Accuracy", 0.761))
788 .metric(ModelMetric::new("Top-5 Accuracy", 0.929))
789 .parameters(25_600_000)
790 .dataset("ImageNet")
791 .author("Deep Learning Team")
792 .tag("vision")
793 .tag("classification")
794 .metadata_entry("license", "Apache-2.0")
795 .width(400.0)
796 .height(300.0)
797 .background(Color::WHITE)
798 .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
799 .corner_radius(12.0)
800 .show_metrics_chart(true)
801 .accessible_name("ResNet-50 model card")
802 .test_id("resnet-card");
803
804 assert_eq!(card.get_name(), "ResNet-50");
805 assert_eq!(card.get_version(), "2.1.0");
806 assert_eq!(card.get_description(), Some("Image classification model"));
807 assert_eq!(card.get_status(), ModelStatus::Published);
808 assert_eq!(card.get_framework(), Some("PyTorch"));
809 assert_eq!(card.get_task(), Some("classification"));
810 assert_eq!(card.get_metrics().len(), 2);
811 assert_eq!(card.get_parameters(), Some(25_600_000));
812 assert_eq!(card.get_dataset(), Some("ImageNet"));
813 assert_eq!(card.get_author(), Some("Deep Learning Team"));
814 assert_eq!(card.get_tags().len(), 2);
815 assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
816 assert_eq!(Widget::accessible_name(&card), Some("ResNet-50 model card"));
817 assert_eq!(Widget::test_id(&card), Some("resnet-card"));
818 }
819
820 #[test]
821 fn test_model_card_metrics() {
822 let metrics = vec![
823 ModelMetric::new("Accuracy", 0.95),
824 ModelMetric::new("F1", 0.92),
825 ];
826 let card = ModelCard::new("Model").metrics(metrics);
827 assert_eq!(card.get_metrics().len(), 2);
828 assert!(card.has_metrics());
829 }
830
831 #[test]
832 fn test_model_card_tags() {
833 let card = ModelCard::new("Model").tags(["nlp", "transformer", "bert"]);
834 assert_eq!(card.get_tags().len(), 3);
835 }
836
837 #[test]
840 fn test_formatted_parameters_none() {
841 let card = ModelCard::new("Model");
842 assert!(card.formatted_parameters().is_none());
843 }
844
845 #[test]
846 fn test_formatted_parameters_small() {
847 let card = ModelCard::new("Model").parameters(500);
848 assert_eq!(card.formatted_parameters(), Some("500".to_string()));
849 }
850
851 #[test]
852 fn test_formatted_parameters_thousands() {
853 let card = ModelCard::new("Model").parameters(25_000);
854 assert_eq!(card.formatted_parameters(), Some("25.0K".to_string()));
855 }
856
857 #[test]
858 fn test_formatted_parameters_millions() {
859 let card = ModelCard::new("Model").parameters(125_000_000);
860 assert_eq!(card.formatted_parameters(), Some("125.0M".to_string()));
861 }
862
863 #[test]
864 fn test_formatted_parameters_billions() {
865 let card = ModelCard::new("Model").parameters(175_000_000_000);
866 assert_eq!(card.formatted_parameters(), Some("175.0B".to_string()));
867 }
868
869 #[test]
872 fn test_model_card_width_min() {
873 let card = ModelCard::new("Model").width(100.0);
874 assert_eq!(card.width, Some(200.0));
875 }
876
877 #[test]
878 fn test_model_card_height_min() {
879 let card = ModelCard::new("Model").height(50.0);
880 assert_eq!(card.height, Some(150.0));
881 }
882
883 #[test]
884 fn test_model_card_corner_radius_min() {
885 let card = ModelCard::new("Model").corner_radius(-5.0);
886 assert_eq!(card.corner_radius, 0.0);
887 }
888
889 #[test]
892 fn test_model_card_type_id() {
893 let card = ModelCard::new("Model");
894 assert_eq!(Widget::type_id(&card), TypeId::of::<ModelCard>());
895 }
896
897 #[test]
898 fn test_model_card_measure_default() {
899 let card = ModelCard::new("Model");
900 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
901 assert_eq!(size.width, 320.0);
902 assert_eq!(size.height, 200.0);
903 }
904
905 #[test]
906 fn test_model_card_measure_custom() {
907 let card = ModelCard::new("Model").width(400.0).height(250.0);
908 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
909 assert_eq!(size.width, 400.0);
910 assert_eq!(size.height, 250.0);
911 }
912
913 #[test]
914 fn test_model_card_layout() {
915 let mut card = ModelCard::new("Model");
916 let bounds = Rect::new(10.0, 20.0, 320.0, 200.0);
917 let result = card.layout(bounds);
918 assert_eq!(result.size, Size::new(320.0, 200.0));
919 assert_eq!(card.bounds, bounds);
920 }
921
922 #[test]
923 fn test_model_card_children() {
924 let card = ModelCard::new("Model");
925 assert!(card.children().is_empty());
926 }
927
928 #[test]
929 fn test_model_card_is_interactive() {
930 let card = ModelCard::new("Model");
931 assert!(!card.is_interactive());
932 }
933
934 #[test]
935 fn test_model_card_is_focusable() {
936 let card = ModelCard::new("Model");
937 assert!(!card.is_focusable());
938 }
939
940 #[test]
941 fn test_model_card_accessible_role() {
942 let card = ModelCard::new("Model");
943 assert_eq!(card.accessible_role(), AccessibleRole::Generic);
944 }
945
946 #[test]
947 fn test_model_card_accessible_name_from_name() {
948 let card = ModelCard::new("GPT-4");
949 assert_eq!(Widget::accessible_name(&card), Some("GPT-4"));
950 }
951
952 #[test]
953 fn test_model_card_accessible_name_explicit() {
954 let card = ModelCard::new("GPT-4").accessible_name("Language model card");
955 assert_eq!(Widget::accessible_name(&card), Some("Language model card"));
956 }
957
958 #[test]
959 fn test_model_card_test_id() {
960 let card = ModelCard::new("Model").test_id("model-card");
961 assert_eq!(Widget::test_id(&card), Some("model-card"));
962 }
963
964 #[test]
967 fn test_model_card_has_metrics_false() {
968 let card = ModelCard::new("Model");
969 assert!(!card.has_metrics());
970 }
971
972 #[test]
973 fn test_model_card_has_metrics_true() {
974 let card = ModelCard::new("Model").metric(ModelMetric::new("Acc", 0.9));
975 assert!(card.has_metrics());
976 }
977
978 #[test]
983 fn test_model_status_color_all_variants() {
984 let _ = ModelStatus::Draft.color();
985 let _ = ModelStatus::Review.color();
986 let _ = ModelStatus::Published.color();
987 let _ = ModelStatus::Deprecated.color();
988 let _ = ModelStatus::Archived.color();
989 }
990
991 #[test]
992 fn test_model_card_children_mut() {
993 let mut card = ModelCard::new("Model");
994 assert!(card.children_mut().is_empty());
995 }
996
997 #[test]
998 fn test_model_card_event_returns_none() {
999 let mut card = ModelCard::new("Model");
1000 let result = card.event(&presentar_core::Event::KeyDown {
1001 key: presentar_core::Key::Down,
1002 });
1003 assert!(result.is_none());
1004 }
1005
1006 #[test]
1007 fn test_model_card_test_id_none() {
1008 let card = ModelCard::new("Model");
1009 assert!(Widget::test_id(&card).is_none());
1010 }
1011
1012 #[test]
1013 fn test_model_card_bounds() {
1014 let mut card = ModelCard::new("Model");
1015 card.layout(Rect::new(5.0, 10.0, 320.0, 200.0));
1016 assert_eq!(card.bounds.x, 5.0);
1017 assert_eq!(card.bounds.y, 10.0);
1018 }
1019
1020 #[test]
1021 fn test_model_metric_eq() {
1022 let m1 = ModelMetric::new("Acc", 0.95);
1023 let m2 = ModelMetric::new("Acc", 0.95);
1024 assert_eq!(m1.name, m2.name);
1025 assert_eq!(m1.value, m2.value);
1026 }
1027
1028 #[test]
1029 fn test_model_card_name_setter() {
1030 let card = ModelCard::new("Initial").name("Changed");
1031 assert_eq!(card.get_name(), "Changed");
1032 }
1033
1034 #[test]
1039 fn test_model_card_brick_name() {
1040 let card = ModelCard::new("test");
1041 assert_eq!(card.brick_name(), "ModelCard");
1042 }
1043
1044 #[test]
1045 fn test_model_card_brick_assertions() {
1046 let card = ModelCard::new("test");
1047 let assertions = card.assertions();
1048 assert!(!assertions.is_empty());
1049 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1050 }
1051
1052 #[test]
1053 fn test_model_card_brick_budget() {
1054 let card = ModelCard::new("test");
1055 let budget = card.budget();
1056 assert!(budget.layout_ms > 0);
1058 assert!(budget.paint_ms > 0);
1059 }
1060
1061 #[test]
1062 fn test_model_card_brick_verify() {
1063 let card = ModelCard::new("test");
1064 let verification = card.verify();
1065 assert!(!verification.passed.is_empty());
1066 assert!(verification.failed.is_empty());
1067 }
1068
1069 #[test]
1070 fn test_model_card_brick_to_html() {
1071 let card = ModelCard::new("test");
1072 let html = card.to_html();
1073 assert!(html.contains("brick-modelcard"));
1074 }
1075
1076 #[test]
1077 fn test_model_card_brick_to_css() {
1078 let card = ModelCard::new("test");
1079 let css = card.to_css();
1080 assert!(css.contains(".brick-modelcard"));
1081 assert!(css.contains("display: block"));
1082 }
1083
1084 #[test]
1085 fn test_model_card_brick_test_id() {
1086 let card = ModelCard::new("test").test_id("card-1");
1087 assert_eq!(Brick::test_id(&card), Some("card-1"));
1088 }
1089
1090 #[test]
1091 fn test_model_card_brick_test_id_none() {
1092 let card = ModelCard::new("test");
1093 assert!(Brick::test_id(&card).is_none());
1094 }
1095
1096 #[test]
1101 fn test_model_status_debug() {
1102 let status = ModelStatus::Published;
1103 let debug_str = format!("{:?}", status);
1104 assert!(debug_str.contains("Published"));
1105 }
1106
1107 #[test]
1108 fn test_model_status_eq() {
1109 assert_eq!(ModelStatus::Draft, ModelStatus::Draft);
1110 assert_ne!(ModelStatus::Draft, ModelStatus::Published);
1111 }
1112
1113 #[test]
1114 fn test_model_status_clone() {
1115 let status = ModelStatus::Review;
1116 let cloned = status;
1117 assert_eq!(cloned, ModelStatus::Review);
1118 }
1119
1120 #[test]
1121 fn test_model_status_serde() {
1122 let status = ModelStatus::Deprecated;
1123 let serialized = serde_json::to_string(&status).unwrap();
1124 let deserialized: ModelStatus = serde_json::from_str(&serialized).unwrap();
1125 assert_eq!(deserialized, ModelStatus::Deprecated);
1126 }
1127
1128 #[test]
1129 fn test_model_status_color_all_variants_detailed() {
1130 let draft_color = ModelStatus::Draft.color();
1132 assert!((draft_color.r - 0.6).abs() < 0.01);
1133
1134 let review_color = ModelStatus::Review.color();
1136 assert!(review_color.r > 0.8);
1137 assert!(review_color.g > 0.6);
1138
1139 let published_color = ModelStatus::Published.color();
1141 assert!(published_color.g > published_color.r);
1142
1143 let deprecated_color = ModelStatus::Deprecated.color();
1145 assert!(deprecated_color.r > 0.8);
1146
1147 let archived_color = ModelStatus::Archived.color();
1149 assert!((archived_color.r - 0.5).abs() < 0.01);
1150 }
1151
1152 #[test]
1157 fn test_model_metric_debug() {
1158 let metric = ModelMetric::new("Accuracy", 0.95);
1159 let debug_str = format!("{:?}", metric);
1160 assert!(debug_str.contains("Accuracy"));
1161 assert!(debug_str.contains("0.95"));
1162 }
1163
1164 #[test]
1165 fn test_model_metric_clone() {
1166 let metric = ModelMetric::new("F1", 0.88).unit("%").lower_is_better();
1167 let cloned = metric.clone();
1168 assert_eq!(cloned.name, "F1");
1169 assert_eq!(cloned.value, 0.88);
1170 assert_eq!(cloned.unit, Some("%".to_string()));
1171 assert!(!cloned.higher_is_better);
1172 }
1173
1174 #[test]
1175 fn test_model_metric_serde() {
1176 let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
1177 let serialized = serde_json::to_string(&metric).unwrap();
1178 let deserialized: ModelMetric = serde_json::from_str(&serialized).unwrap();
1179 assert_eq!(deserialized.name, "Loss");
1180 assert_eq!(deserialized.value, 0.05);
1181 assert!(!deserialized.higher_is_better);
1182 }
1183
1184 #[test]
1185 fn test_model_metric_formatted_value_negative() {
1186 let metric = ModelMetric::new("Correlation", -0.5);
1187 let formatted = metric.formatted_value();
1188 assert!(formatted.contains("-50.00%"));
1189 }
1190
1191 #[test]
1192 fn test_model_metric_formatted_value_zero() {
1193 let metric = ModelMetric::new("Bias", 0.0);
1194 assert_eq!(metric.formatted_value(), "0.00%");
1195 }
1196
1197 #[test]
1198 fn test_model_metric_formatted_value_exactly_one() {
1199 let metric = ModelMetric::new("Perfect", 1.0);
1200 assert_eq!(metric.formatted_value(), "1.00");
1201 }
1202
1203 #[test]
1208 fn test_model_card_debug() {
1209 let card = ModelCard::new("GPT-4");
1210 let debug_str = format!("{:?}", card);
1211 assert!(debug_str.contains("GPT-4"));
1212 }
1213
1214 #[test]
1215 fn test_model_card_clone() {
1216 let card = ModelCard::new("BERT")
1217 .version("2.0.0")
1218 .status(ModelStatus::Published)
1219 .framework("PyTorch");
1220 let cloned = card.clone();
1221 assert_eq!(cloned.get_name(), "BERT");
1222 assert_eq!(cloned.get_version(), "2.0.0");
1223 assert_eq!(cloned.get_status(), ModelStatus::Published);
1224 assert_eq!(cloned.get_framework(), Some("PyTorch"));
1225 }
1226
1227 #[test]
1228 fn test_model_card_serde() {
1229 let card = ModelCard::new("ResNet")
1230 .version("1.0.0")
1231 .status(ModelStatus::Draft);
1232 let serialized = serde_json::to_string(&card).unwrap();
1233 let deserialized: ModelCard = serde_json::from_str(&serialized).unwrap();
1234 assert_eq!(deserialized.get_name(), "ResNet");
1235 assert_eq!(deserialized.get_version(), "1.0.0");
1236 assert_eq!(deserialized.get_status(), ModelStatus::Draft);
1237 }
1238
1239 #[test]
1244 fn test_model_card_getters_none() {
1245 let card = ModelCard::new("test");
1246 assert!(card.get_description().is_none());
1247 assert!(card.get_framework().is_none());
1248 assert!(card.get_task().is_none());
1249 assert!(card.get_parameters().is_none());
1250 assert!(card.get_dataset().is_none());
1251 assert!(card.get_author().is_none());
1252 assert!(card.get_metadata("nonexistent").is_none());
1253 }
1254
1255 #[test]
1256 fn test_model_card_getters_some() {
1257 let card = ModelCard::new("test")
1258 .description("desc")
1259 .framework("TensorFlow")
1260 .task("classification")
1261 .parameters(1_000_000)
1262 .dataset("CIFAR-10")
1263 .author("ML Team")
1264 .metadata_entry("license", "Apache-2.0");
1265
1266 assert_eq!(card.get_description(), Some("desc"));
1267 assert_eq!(card.get_framework(), Some("TensorFlow"));
1268 assert_eq!(card.get_task(), Some("classification"));
1269 assert_eq!(card.get_parameters(), Some(1_000_000));
1270 assert_eq!(card.get_dataset(), Some("CIFAR-10"));
1271 assert_eq!(card.get_author(), Some("ML Team"));
1272 assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
1273 }
1274
1275 #[test]
1280 fn test_model_card_measure_with_tight_constraints() {
1281 let card = ModelCard::new("test").width(400.0).height(300.0);
1282 let size = card.measure(Constraints::tight(Size::new(200.0, 150.0)));
1283 assert_eq!(size.width, 200.0);
1284 assert_eq!(size.height, 150.0);
1285 }
1286
1287 #[test]
1292 fn test_model_card_empty_metrics() {
1293 let card = ModelCard::new("test").metrics(vec![]);
1294 assert!(!card.has_metrics());
1295 assert!(card.get_metrics().is_empty());
1296 }
1297
1298 #[test]
1299 fn test_model_card_many_metrics() {
1300 let metrics: Vec<ModelMetric> = (0..5)
1301 .map(|i| ModelMetric::new(format!("metric_{i}"), i as f64 * 0.1))
1302 .collect();
1303 let card = ModelCard::new("test").metrics(metrics);
1304 assert!(card.has_metrics());
1305 assert_eq!(card.get_metrics().len(), 5);
1306 }
1307
1308 #[test]
1309 fn test_model_card_empty_tags() {
1310 let tags: [&str; 0] = [];
1311 let card = ModelCard::new("test").tags(tags);
1312 assert!(card.get_tags().is_empty());
1313 }
1314
1315 #[test]
1316 fn test_model_card_show_metrics_chart_false() {
1317 let card = ModelCard::new("test")
1318 .metric(ModelMetric::new("acc", 0.95))
1319 .show_metrics_chart(false);
1320 assert!(card.has_metrics());
1321 }
1323
1324 #[test]
1325 fn test_model_card_default_colors() {
1326 let card = ModelCard::default();
1327 assert_eq!(card.background, Color::WHITE);
1328 }
1329
1330 #[test]
1331 fn test_model_card_default_values() {
1332 let card = ModelCard::default();
1333 assert!(card.name.is_empty());
1334 assert_eq!(card.version, "1.0.0");
1335 assert_eq!(card.status, ModelStatus::Draft);
1336 assert!(card.show_metrics_chart);
1337 assert_eq!(card.corner_radius, 8.0);
1338 }
1339
1340 #[test]
1345 fn test_formatted_parameters_edge_cases() {
1346 let card = ModelCard::new("test").parameters(1000);
1348 assert_eq!(card.formatted_parameters(), Some("1.0K".to_string()));
1349
1350 let card = ModelCard::new("test").parameters(1_000_000);
1352 assert_eq!(card.formatted_parameters(), Some("1.0M".to_string()));
1353
1354 let card = ModelCard::new("test").parameters(1_000_000_000);
1356 assert_eq!(card.formatted_parameters(), Some("1.0B".to_string()));
1357 }
1358}