1use crate::session::messages as msg;
8use crate::session::question::{Question, QuestionStep};
9use crate::session::state::ClassificationState;
10use crate::types::{Language, OrganicInorganic, PhysicalForm};
11
12pub fn next_question(
15 state: &ClassificationState,
16 lang: Language,
17) -> Option<(Question, QuestionStep)> {
18 if !state.has_identifier() {
20 let (prompt, example) = msg::q_identifier(lang);
21 return Some((
22 Question::Text { prompt, example: Some(example) },
23 QuestionStep::Identifier,
24 ));
25 }
26
27 if state.is_mixture.is_none() {
29 return Some((
30 Question::YesNo { prompt: msg::q_is_mixture(lang) },
31 QuestionStep::IsMixture,
32 ));
33 }
34
35 if state.is_mixture == Some(true) {
37 if state.component_count.is_none() {
38 let (prompt, unit) = msg::q_component_count(lang);
39 return Some((
40 Question::Number { prompt, unit, min: 2.0, max: 20.0 },
41 QuestionStep::ComponentCount,
42 ));
43 }
44
45 let expected = state.component_count.unwrap_or(0);
46 let idx = state.current_component_index;
47
48 if idx < expected && !state.current_component_has_identifier() {
50 let (prompt, example) = msg::q_component_identifier(lang, idx + 1);
51 return Some((
52 Question::Text { prompt, example: Some(example) },
53 QuestionStep::ComponentIdentifier,
54 ));
55 }
56
57 if idx < expected && !state.current_component_has_fraction() {
59 let name = state
60 .components
61 .get(idx)
62 .and_then(|c| c.identifier.iupac_name.clone())
63 .or_else(|| state.components.get(idx).and_then(|c| c.identifier.cas.clone()))
64 .unwrap_or_else(|| format!("component {}", idx + 1));
65 let (prompt, unit) = msg::q_component_fraction(lang, &name);
66 return Some((
67 Question::Number { prompt, unit, min: 0.0, max: 100.0 },
68 QuestionStep::ComponentFraction,
69 ));
70 }
71
72 if state.components.len() < expected {
74 let next_idx = state.components.len();
75 let (prompt, example) = msg::q_next_component_identifier(lang, next_idx + 1);
76 return Some((
77 Question::Text { prompt, example: Some(example) },
78 QuestionStep::ComponentIdentifier,
79 ));
80 }
81
82 return None;
84 }
85
86 if state.physical_form.is_none() {
90 let (prompt, options) = msg::q_physical_form(lang);
91 return Some((
92 Question::Choice { prompt, options },
93 QuestionStep::PhysicalForm,
94 ));
95 }
96
97 if let Some(PhysicalForm::Solution { concentration_pct_ww: None, .. }) = &state.physical_form {
99 let (prompt, unit) = msg::q_solution_concentration(lang);
100 return Some((
101 Question::Number { prompt, unit, min: 0.0, max: 100.0 },
102 QuestionStep::SolutionConcentration,
103 ));
104 }
105
106 if state.intended_use.is_none() {
108 let (prompt, options) = msg::q_intended_use(lang);
109 return Some((
110 Question::Choice { prompt, options },
111 QuestionStep::IntendedUse,
112 ));
113 }
114
115 if state.organic_inorganic.is_none() && state.identifier.smiles.is_none() {
117 let (prompt, options) = msg::q_organic_inorganic(lang);
118 return Some((
119 Question::Choice { prompt, options },
120 QuestionStep::OrganicInorganic,
121 ));
122 }
123
124 if state.identifier.smiles.is_none()
126 && matches!(state.organic_inorganic, Some(OrganicInorganic::Organic))
127 && state.detected_functional_groups.is_empty()
128 && state.chapter_hint.is_none()
129 {
130 let (prompt, options) = msg::q_functional_groups(lang);
131 return Some((
132 Question::MultiChoice { prompt, options, include_unknown: true },
133 QuestionStep::FunctionalGroups,
134 ));
135 }
136
137 None
139}
140
141pub fn choice_index_to_physical_form(index: usize) -> PhysicalForm {
147 match index {
148 0 => PhysicalForm::Solid,
149 1 => PhysicalForm::Powder { particle_size_um: None },
150 2 => PhysicalForm::Granules,
151 3 => PhysicalForm::Liquid,
152 4 => PhysicalForm::Solution { solvent: None, concentration_pct_ww: None },
153 5 => PhysicalForm::Gas,
154 6 => PhysicalForm::Foil { thickness_mm: None },
155 7 => PhysicalForm::Ingot,
156 _ => PhysicalForm::Unknown,
157 }
158}
159
160pub fn choice_index_to_intended_use(index: usize) -> crate::types::IntendedUse {
162 use crate::types::IntendedUse;
163 match index {
164 0 => IntendedUse::Industrial,
165 1 => IntendedUse::Pharmaceutical,
166 2 => IntendedUse::Agricultural,
167 3 => IntendedUse::Food,
168 4 => IntendedUse::Cosmetic,
169 _ => IntendedUse::Other("unknown".to_string()),
170 }
171}
172
173pub fn choice_index_to_organic_inorganic(index: usize) -> OrganicInorganic {
175 match index {
176 0 => OrganicInorganic::Organic,
177 1 => OrganicInorganic::Inorganic,
178 _ => OrganicInorganic::Unknown,
179 }
180}
181
182pub fn multi_choice_indices_to_functional_groups(indices: &[usize]) -> Vec<String> {
184 const GROUPS: &[&str] = &[
185 "carboxylic_acid",
186 "alcohol",
187 "phenol",
188 "aldehyde",
189 "ketone",
190 "amine",
191 "amide",
192 "nitrile",
193 "halide",
194 "ester",
195 "aromatic",
196 ];
197 indices
198 .iter()
199 .filter_map(|&i| GROUPS.get(i).map(|s| s.to_string()))
200 .collect()
201}