flams_ontology/narration/
problems.rs

1use flams_utils::CSS;
2use smallvec::SmallVec;
3use std::{collections::HashMap, fmt::Display, str::FromStr};
4
5use crate::{
6    uris::{DocumentElementURI, Name, SymbolURI},
7    Checked, CheckingState, DocumentRange,
8};
9
10use super::{DocumentElement, LazyDocRef, NarrationTrait};
11
12#[cfg(feature = "wasm")]
13use wasm_bindgen::prelude::wasm_bindgen;
14
15#[derive(Debug)]
16pub struct Problem<State: CheckingState> {
17    pub sub_problem: bool,
18    pub uri: DocumentElementURI,
19    pub range: DocumentRange,
20    pub autogradable: bool,
21    pub points: Option<f32>,
22    pub solutions: LazyDocRef<Solutions>, //State::Seq<SolutionData>,
23    pub gnotes: State::Seq<LazyDocRef<GradingNote>>,
24    pub hints: State::Seq<DocumentRange>,
25    pub notes: State::Seq<LazyDocRef<Box<str>>>,
26    pub title: Option<DocumentRange>,
27    pub children: State::Seq<DocumentElement<State>>,
28    pub styles: Box<[Name]>,
29    pub preconditions: State::Seq<(CognitiveDimension, SymbolURI)>,
30    pub objectives: State::Seq<(CognitiveDimension, SymbolURI)>,
31}
32
33#[cfg(not(feature = "wasm"))]
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Solutions(Box<[SolutionData]>);
37
38#[cfg(feature = "wasm")]
39#[allow(clippy::unsafe_derive_deserialize)]
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
41#[wasm_bindgen]
42pub struct Solutions(Box<[SolutionData]>);
43
44#[cfg_attr(feature = "wasm", wasm_bindgen)]
45impl Solutions {
46    #[cfg(feature = "serde")]
47    #[must_use]
48    pub fn from_jstring(s: &str) -> Option<Self> {
49        use flams_utils::Hexable;
50        Self::from_hex(s).ok()
51    }
52    #[cfg(feature = "serde")]
53    #[must_use]
54    pub fn to_jstring(&self) -> Option<String> {
55        use flams_utils::Hexable;
56        self.as_hex().ok()
57    }
58
59    #[inline]
60    pub fn from_solutions(solutions: Box<[SolutionData]>) -> Self {
61        Self(solutions)
62    }
63
64    #[inline]
65    pub fn to_solutions(&self) -> Box<[SolutionData]> {
66        self.0.clone()
67    }
68
69    #[must_use]
70    #[inline]
71    pub fn check_response(&self, response: &ProblemResponse) -> Option<ProblemFeedback> {
72        self.check(response)
73    }
74
75    #[must_use]
76    #[inline]
77    pub fn default_feedback(&self) -> ProblemFeedback {
78        self.default()
79    }
80}
81
82impl crate::Resourcable for Solutions {}
83impl Solutions {
84    #[must_use]
85    pub fn default(&self) -> ProblemFeedback {
86        let mut solutions = SmallVec::new();
87        let mut data = SmallVec::new();
88        for sol in self.0.iter() {
89            match sol {
90                SolutionData::Solution { html, .. } => solutions.push(html.clone()),
91                SolutionData::ChoiceBlock(ChoiceBlock {
92                    multiple: false,
93                    choices,
94                    ..
95                }) => data.push(CheckedResult::SingleChoice {
96                    selected: None,
97                    choices: choices
98                        .iter()
99                        .enumerate()
100                        .map(|(_, c)| BlockFeedback {
101                            is_correct: c.correct,
102                            verdict_str: c.verdict.to_string(),
103                            feedback: c.feedback.to_string(),
104                        })
105                        .collect(),
106                }),
107                SolutionData::ChoiceBlock(ChoiceBlock { choices, .. }) => {
108                    data.push(CheckedResult::MultipleChoice {
109                        selected: choices.iter().map(|_| false).collect(),
110                        choices: choices
111                            .iter()
112                            .enumerate()
113                            .map(|(_, c)| BlockFeedback {
114                                is_correct: c.correct,
115                                verdict_str: c.verdict.to_string(),
116                                feedback: c.feedback.to_string(),
117                            })
118                            .collect(),
119                    })
120                }
121                SolutionData::FillInSol(f) => {
122                    let mut options = SmallVec::new();
123                    for o in f.opts.iter() {
124                        match o {
125                            FillInSolOption::Exact {
126                                value,
127                                verdict,
128                                feedback,
129                            } => options.push(FillinFeedback {
130                                is_correct: *verdict,
131                                feedback: feedback.to_string(),
132                                kind: FillinFeedbackKind::Exact(value.to_string()),
133                            }),
134                            FillInSolOption::NumericalRange {
135                                from,
136                                to,
137                                verdict,
138                                feedback,
139                            } => options.push(FillinFeedback {
140                                is_correct: *verdict,
141                                feedback: feedback.to_string(),
142                                kind: FillinFeedbackKind::NumRange {
143                                    from: *from,
144                                    to: *to,
145                                },
146                            }),
147                            FillInSolOption::Regex {
148                                regex,
149                                verdict,
150                                feedback,
151                            } => options.push(FillinFeedback {
152                                is_correct: *verdict,
153                                feedback: feedback.to_string(),
154                                kind: FillinFeedbackKind::Regex(regex.as_str().to_string()),
155                            }),
156                        }
157                    }
158                    data.push(CheckedResult::FillinSol {
159                        matching: None,
160                        options,
161                        text: String::new(),
162                    });
163                }
164            }
165        }
166
167        ProblemFeedback {
168            correct: false,
169            solutions,
170            data,
171            score_fraction: 0.0,
172        }
173    }
174
175    #[must_use]
176    #[allow(clippy::too_many_lines)]
177    #[allow(clippy::cast_precision_loss)]
178    pub fn check(&self, response: &ProblemResponse) -> Option<ProblemFeedback> {
179        //println!("Here: {self:?}\n{response:?}");
180        fn next_sol<'a>(
181            solutions: &mut SmallVec<Box<str>, 1>,
182            datas: &mut impl Iterator<Item = &'a SolutionData>,
183        ) -> Option<&'a SolutionData> {
184            loop {
185                match datas.next() {
186                    None => return None,
187                    Some(SolutionData::Solution { html, .. }) => solutions.push(html.clone()),
188                    Some(c) => return Some(c),
189                }
190            }
191        }
192        let mut correct = true;
193        let mut pts: f32 = 0.0;
194        let mut total: f32 = 0.0;
195        let mut solutions = SmallVec::new();
196        let mut data = SmallVec::new();
197        let mut datas = self.0.iter();
198
199        for response in &response.responses {
200            total += 1.0;
201            let sol = next_sol(&mut solutions, &mut datas)?;
202            match (response, sol) {
203                (
204                    ProblemResponseType::SingleChoice { value: selected },
205                    SolutionData::ChoiceBlock(ChoiceBlock {
206                        multiple: false,
207                        choices,
208                        ..
209                    }),
210                ) => data.push(CheckedResult::SingleChoice {
211                    selected: *selected,
212                    choices: choices
213                        .iter()
214                        .enumerate()
215                        .map(
216                            |(
217                                i,
218                                Choice {
219                                    correct: cr,
220                                    verdict,
221                                    feedback,
222                                },
223                            )| {
224                                if selected.is_some_and(|j| j as usize == i) {
225                                    correct = correct && *cr;
226                                    if *cr {
227                                        pts += 1.0;
228                                    }
229                                }
230                                BlockFeedback {
231                                    is_correct: *cr,
232                                    verdict_str: verdict.to_string(),
233                                    feedback: feedback.to_string(),
234                                }
235                            },
236                        )
237                        .collect(),
238                }),
239                (
240                    ProblemResponseType::MultipleChoice { value: selected },
241                    SolutionData::ChoiceBlock(ChoiceBlock {
242                        multiple: true,
243                        choices,
244                        ..
245                    }),
246                ) => {
247                    if selected.len() != choices.len() {
248                        return None;
249                    }
250                    let mut corrects = 0;
251                    let mut falses = 0;
252                    data.push(CheckedResult::MultipleChoice {
253                        selected: selected.clone(),
254                        choices: choices
255                            .iter()
256                            .enumerate()
257                            .map(
258                                |(
259                                    i,
260                                    Choice {
261                                        correct: cr,
262                                        verdict,
263                                        feedback,
264                                    },
265                                )| {
266                                    if *cr == selected[i] {
267                                        corrects += 1;
268                                    } else {
269                                        falses += 1;
270                                    }
271                                    correct = correct && (selected[i] == *cr);
272                                    BlockFeedback {
273                                        is_correct: *cr,
274                                        verdict_str: verdict.to_string(),
275                                        feedback: feedback.to_string(),
276                                    }
277                                },
278                            )
279                            .collect(),
280                    });
281                    if selected.iter().any(|b| *b) {
282                        pts += ((corrects as f32 - falses as f32) / choices.len() as f32).max(0.0);
283                    }
284                }
285                (ProblemResponseType::Fillinsol { value: s }, SolutionData::FillInSol(f)) => {
286                    let mut fill_correct = None;
287                    let mut matching = None;
288                    let mut options = SmallVec::new();
289                    for (i, o) in f.opts.iter().enumerate() {
290                        match o {
291                            FillInSolOption::Exact {
292                                value: string,
293                                verdict,
294                                feedback,
295                            } => {
296                                if fill_correct.is_none() && &**string == s.as_str() {
297                                    if *verdict {
298                                        pts += 1.0;
299                                    }
300                                    fill_correct = Some(*verdict);
301                                    matching = Some(i);
302                                }
303                                options.push(FillinFeedback {
304                                    is_correct: *verdict,
305                                    feedback: feedback.to_string(),
306                                    kind: FillinFeedbackKind::Exact(string.to_string()),
307                                });
308                            }
309                            FillInSolOption::NumericalRange {
310                                from,
311                                to,
312                                verdict,
313                                feedback,
314                            } => {
315                                if fill_correct.is_none() {
316                                    let num = if s.contains('.') {
317                                        s.parse::<f32>().ok()
318                                    } else {
319                                        s.parse::<i32>().ok().map(|i| i as f32)
320                                    };
321                                    if let Some(f) = num {
322                                        if !from.is_some_and(|v| f < v)
323                                            && !to.is_some_and(|v| f > v)
324                                        {
325                                            if *verdict {
326                                                pts += 1.0;
327                                            }
328                                            fill_correct = Some(*verdict);
329                                            matching = Some(i);
330                                        }
331                                    }
332                                }
333                                options.push(FillinFeedback {
334                                    is_correct: *verdict,
335                                    feedback: feedback.to_string(),
336                                    kind: FillinFeedbackKind::NumRange {
337                                        from: *from,
338                                        to: *to,
339                                    },
340                                });
341                            }
342                            FillInSolOption::Regex {
343                                regex,
344                                verdict,
345                                feedback,
346                            } => {
347                                if fill_correct.is_none() && regex.is_match(s) {
348                                    if *verdict {
349                                        pts += 1.0;
350                                    }
351                                    fill_correct = Some(*verdict);
352                                    matching = Some(i);
353                                }
354                                options.push(FillinFeedback {
355                                    is_correct: *verdict,
356                                    feedback: feedback.to_string(),
357                                    kind: FillinFeedbackKind::Regex(regex.as_str().to_string()),
358                                });
359                            }
360                        }
361                    }
362                    correct = correct && fill_correct.unwrap_or_default();
363                    data.push(CheckedResult::FillinSol {
364                        matching,
365                        options,
366                        text: s.to_string(),
367                    });
368                }
369                _ => return None,
370            }
371        }
372
373        if next_sol(&mut solutions, &mut datas).is_some() {
374            return None;
375        }
376
377        Some(ProblemFeedback {
378            correct,
379            solutions,
380            data,
381            score_fraction: pts / total,
382        })
383    }
384}
385
386crate::serde_impl! {
387    struct Problem[
388        sub_problem,uri,range,autogradable,points,solutions,gnotes,
389        hints,notes,title,children,styles,preconditions,
390        objectives
391    ]
392}
393
394#[derive(Debug, Clone)]
395#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
396#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
397#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
398pub enum SolutionData {
399    Solution {
400        html: Box<str>,
401        answer_class: Option<Box<str>>,
402    },
403    ChoiceBlock(ChoiceBlock),
404    FillInSol(FillInSol),
405}
406
407#[derive(Debug, Clone)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
410#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
411pub struct ChoiceBlock {
412    pub multiple: bool,
413    pub inline: bool,
414    pub range: DocumentRange,
415    pub styles: Box<[Box<str>]>,
416    pub choices: Vec<Choice>,
417}
418
419#[derive(Debug, Clone)]
420#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
421#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
422#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
423pub struct Choice {
424    pub correct: bool,
425    pub verdict: Box<str>,
426    pub feedback: Box<str>,
427}
428
429#[derive(Debug, Clone)]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
432#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
433pub struct FillInSol {
434    pub width: Option<f32>,
435    pub opts: Vec<FillInSolOption>,
436}
437
438#[derive(Debug, Clone)]
439#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
440#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
441#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
442pub enum FillInSolOption {
443    Exact {
444        value: Box<str>,
445        verdict: bool,
446        feedback: Box<str>,
447    },
448    NumericalRange {
449        from: Option<f32>,
450        to: Option<f32>,
451        verdict: bool,
452        feedback: Box<str>,
453    },
454    Regex {
455        regex: flams_utils::regex::Regex,
456        verdict: bool,
457        feedback: Box<str>,
458    },
459}
460impl FillInSolOption {
461    #[allow(clippy::cast_precision_loss)]
462    #[must_use]
463    pub fn from_values(kind: &str, value: &str, verdict: bool) -> Option<Self> {
464        match kind {
465            "exact" => Some(Self::Exact {
466                value: value.to_string().into(),
467                verdict,
468                feedback: String::new().into(),
469            }),
470            "numrange" => {
471                let (s, neg) = value
472                    .strip_prefix('-')
473                    .map_or((value, false), |s| (s, true));
474                let (from, to) = if let Some((from, to)) = s.split_once('-') {
475                    (from, to)
476                } else {
477                    ("", s)
478                };
479                let from = if from.contains('.') {
480                    Some(f32::from_str(from).ok()?)
481                } else if from.is_empty() {
482                    None
483                } else {
484                    Some(i128::from_str(from).ok()? as _)
485                };
486                let from = if neg { from.map(|f| -f) } else { from };
487                let to = if to.contains('.') {
488                    Some(f32::from_str(to).ok()?)
489                } else if to.is_empty() {
490                    None
491                } else {
492                    Some(i128::from_str(to).ok()? as _)
493                };
494                Some(Self::NumericalRange {
495                    from,
496                    to,
497                    verdict,
498                    feedback: String::new().into(),
499                })
500            }
501            "regex" => Some(Self::Regex {
502                regex: flams_utils::regex::Regex::new(
503                    value, //&format!("^{value}?")
504                )
505                .ok()?,
506                verdict,
507                feedback: String::new().into(),
508            }),
509            _ => None,
510        }
511    }
512}
513
514#[derive(Debug, Clone)]
515#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
516pub struct GradingNote {
517    pub html: Box<str>,
518    pub answer_classes: Vec<AnswerClass>,
519}
520impl crate::Resourcable for GradingNote {}
521
522#[cfg(not(feature = "wasm"))]
523#[derive(Debug, Clone)]
524#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
525pub struct ProblemFeedback {
526    pub correct: bool,
527    pub solutions: SmallVec<Box<str>, 1>,
528    pub data: SmallVec<CheckedResult, 4>,
529    pub score_fraction: f32,
530}
531
532#[cfg(feature = "wasm")]
533#[allow(clippy::unsafe_derive_deserialize)]
534#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
535#[wasm_bindgen]
536pub struct ProblemFeedback {
537    pub correct: bool,
538    #[wasm_bindgen(skip)]
539    pub solutions: SmallVec<Box<str>, 1>,
540    //#[cfg_attr(feature="wasm", tsify(type = "CheckedResult[]"))]
541    #[wasm_bindgen(skip)]
542    pub data: SmallVec<CheckedResult, 4>,
543    pub score_fraction: f32,
544}
545
546#[cfg(feature = "wasm")]
547#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
548#[tsify(from_wasm_abi, into_wasm_abi)]
549pub struct ProblemFeedbackJson {
550    pub correct: bool,
551    #[cfg_attr(feature = "wasm", tsify(type = "string[]"))]
552    pub solutions: SmallVec<Box<str>, 1>,
553    #[cfg_attr(feature = "wasm", tsify(type = "CheckedResult[]"))]
554    pub data: SmallVec<CheckedResult, 4>,
555    pub score_fraction: f32,
556}
557
558#[cfg(feature = "wasm")]
559#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
560impl ProblemFeedback {
561    #[must_use]
562    pub fn from_jstring(s: &str) -> Option<Self> {
563        use flams_utils::Hexable;
564        Self::from_hex(s).ok()
565    }
566
567    #[cfg(feature = "serde")]
568    #[must_use]
569    pub fn to_jstring(&self) -> Option<String> {
570        use flams_utils::Hexable;
571        self.as_hex().ok()
572    }
573
574    #[inline]
575    pub fn from_json(
576        ProblemFeedbackJson {
577            correct,
578            solutions,
579            data,
580            score_fraction,
581        }: ProblemFeedbackJson,
582    ) -> Self {
583        Self {
584            correct,
585            solutions,
586            data,
587            score_fraction,
588        }
589    }
590
591    #[inline]
592    pub fn to_json(&self) -> ProblemFeedbackJson {
593        let Self {
594            correct,
595            solutions,
596            data,
597            score_fraction,
598        } = self.clone();
599        ProblemFeedbackJson {
600            correct,
601            solutions,
602            data,
603            score_fraction,
604        }
605    }
606}
607
608#[derive(Debug, Clone)]
609#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
610#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
611#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
612pub struct BlockFeedback {
613    pub is_correct: bool,
614    pub verdict_str: String,
615    pub feedback: String,
616}
617
618#[derive(Debug, Clone)]
619#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
620#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
621#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
622pub struct FillinFeedback {
623    pub is_correct: bool,
624    pub feedback: String,
625    pub kind: FillinFeedbackKind,
626}
627
628#[derive(Debug, Clone)]
629#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
630#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
631#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
632pub enum FillinFeedbackKind {
633    Exact(String),
634    NumRange { from: Option<f32>, to: Option<f32> },
635    Regex(String),
636}
637
638#[derive(Debug, Clone)]
639#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
640#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
641#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
642#[cfg_attr(feature = "serde", serde(tag = "type"))]
643pub enum CheckedResult {
644    SingleChoice {
645        selected: Option<u16>,
646        #[cfg_attr(feature = "wasm", tsify(type = "BlockFeedback[]"))]
647        choices: SmallVec<BlockFeedback, 4>,
648    },
649    MultipleChoice {
650        #[cfg_attr(feature = "wasm", tsify(type = "boolean[]"))]
651        selected: SmallVec<bool, 8>,
652        #[cfg_attr(feature = "wasm", tsify(type = "BlockFeedback[]"))]
653        choices: SmallVec<BlockFeedback, 4>,
654    },
655    FillinSol {
656        matching: Option<usize>,
657        text: String,
658        #[cfg_attr(feature = "wasm", tsify(type = "FillinFeedback[]"))]
659        options: SmallVec<FillinFeedback, 4>,
660    },
661}
662
663#[derive(Debug, Clone)]
664#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
665#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
666#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
667//#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
668pub struct ProblemResponse {
669    pub uri: DocumentElementURI,
670    #[cfg_attr(feature = "wasm", tsify(type = "ProblemResponseType[]"))]
671    pub responses: SmallVec<ProblemResponseType, 4>,
672}
673
674#[derive(Debug, Clone)]
675#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
676#[cfg_attr(feature = "serde", serde(tag = "type"))]
677#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
678#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
679/// Either a list of booleans (multiple choice), a single integer (single choice),
680/// or a string (fill-in-the-gaps)
681pub enum ProblemResponseType {
682    MultipleChoice {
683        #[cfg_attr(feature = "wasm", tsify(type = "boolean[]"))]
684        value: SmallVec<bool, 8>,
685    },
686    SingleChoice {
687        value: Option<u16>,
688    },
689    Fillinsol {
690        #[serde(rename = "value")]
691        value: String,
692    },
693}
694
695#[derive(Debug, Clone)]
696#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
697#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
698#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
699pub struct AnswerClass {
700    pub id: Box<str>,
701    pub feedback: Box<str>,
702    pub kind: AnswerKind,
703}
704
705#[derive(Debug, Clone, Copy)]
706#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
707#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
708#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
709pub enum AnswerKind {
710    Class(f32),
711    Trait(f32),
712}
713impl FromStr for AnswerKind {
714    type Err = ();
715    fn from_str(s: &str) -> Result<Self, Self::Err> {
716        #[allow(clippy::cast_precision_loss)]
717        fn num(s: &str) -> Result<f32, ()> {
718            if s.contains('.') {
719                s.parse().map_err(|_| ())
720            } else {
721                let i: Result<i32, ()> = s.parse().map_err(|_| ());
722                i.map(|i| i as f32)
723            }
724        }
725        let s = s.trim();
726        s.strip_prefix('+').map_or_else(
727            || {
728                s.strip_prefix('-').map_or_else(
729                    || num(s).map(AnswerKind::Class),
730                    |s| num(s).map(|f| Self::Trait(-f)),
731                )
732            },
733            |s| num(s).map(AnswerKind::Trait),
734        )
735    }
736}
737
738impl NarrationTrait for Problem<Checked> {
739    #[inline]
740    fn children(&self) -> &[DocumentElement<Checked>] {
741        &self.children
742    }
743    #[inline]
744    fn from_element(e: &DocumentElement<Checked>) -> Option<&Self>
745    where
746        Self: Sized,
747    {
748        if let DocumentElement::Problem(e) = e {
749            Some(e)
750        } else {
751            None
752        }
753    }
754}
755
756#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
757#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
758#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
759#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
760pub enum CognitiveDimension {
761    Remember,
762    Understand,
763    Apply,
764    Analyze,
765    Evaluate,
766    Create,
767}
768impl CognitiveDimension {
769    #[cfg(feature = "rdf")]
770    #[must_use]
771    pub const fn to_iri(&self) -> crate::rdf::NamedNodeRef {
772        use crate::rdf::ontologies::ulo2;
773        use CognitiveDimension::*;
774        match self {
775            Remember => ulo2::REMEMBER,
776            Understand => ulo2::UNDERSTAND,
777            Apply => ulo2::APPLY,
778            Analyze => ulo2::ANALYZE,
779            Evaluate => ulo2::EVALUATE,
780            Create => ulo2::CREATE,
781        }
782    }
783}
784impl Display for CognitiveDimension {
785    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
786        use CognitiveDimension::*;
787        write!(
788            f,
789            "{}",
790            match self {
791                Remember => "remember",
792                Understand => "understand",
793                Apply => "apply",
794                Analyze => "analyze",
795                Evaluate => "evaluate",
796                Create => "create",
797            }
798        )
799    }
800}
801impl FromStr for CognitiveDimension {
802    type Err = ();
803    fn from_str(s: &str) -> Result<Self, Self::Err> {
804        use CognitiveDimension::*;
805        Ok(match s {
806            "remember" => Remember,
807            "understand" => Understand,
808            "apply" => Apply,
809            "analyze" | "analyse" => Analyze,
810            "evaluate" => Evaluate,
811            "create" => Create,
812            _ => return Err(()),
813        })
814    }
815}
816
817#[derive(Debug, Clone)]
818#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
819#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
820#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
821pub struct Quiz {
822    pub css: Vec<CSS>,
823    pub title: Option<String>,
824    pub elements: Vec<QuizElement>,
825    pub solutions: HashMap<DocumentElementURI, String>,
826    pub answer_classes: HashMap<DocumentElementURI, Vec<AnswerClass>>,
827}
828
829#[derive(Debug, Clone)]
830#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
831#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
832#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
833pub enum QuizElement {
834    Section {
835        title: String,
836        elements: Vec<QuizElement>,
837    },
838    Problem(QuizProblem),
839    Paragraph {
840        html: String,
841    },
842}
843
844#[derive(Debug, Clone)]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
847#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
848pub struct QuizProblem {
849    pub html: String,
850    pub title_html: Option<String>,
851    pub uri: DocumentElementURI,
852    //pub solution:String,//Solutions,
853    pub total_points: Option<f32>,
854    //pub is_sub_problem:bool,
855    pub preconditions: Vec<(CognitiveDimension, SymbolURI)>,
856    pub objectives: Vec<(CognitiveDimension, SymbolURI)>,
857}