1use crate::humanize::Lang;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Persona {
13 Enfant,
14 Enseignant,
15 GrandMere,
16 Artisan,
17 Maison,
18 Builder,
19 Developpeur,
20 Expert,
21}
22
23impl Persona {
24 pub fn slug(&self) -> &'static str {
26 match self {
27 Persona::Enfant => "enfant",
28 Persona::Enseignant => "enseignant",
29 Persona::GrandMere => "grand-mere",
30 Persona::Artisan => "artisan",
31 Persona::Maison => "maison",
32 Persona::Builder => "builder",
33 Persona::Developpeur => "developpeur",
34 Persona::Expert => "expert",
35 }
36 }
37
38 pub fn label(&self, lang: Lang) -> &'static str {
39 match (self, lang) {
40 (Persona::Enfant, Lang::Fr) => "Pour un enfant",
41 (Persona::Enfant, Lang::En) => "For a child",
42 (Persona::Enseignant, Lang::Fr) => "Pour un enseignant",
43 (Persona::Enseignant, Lang::En) => "For a teacher",
44 (Persona::GrandMere, Lang::Fr) => "Pour une grand-mère",
45 (Persona::GrandMere, Lang::En) => "For a grandparent",
46 (Persona::Artisan, Lang::Fr) => "Pour un artisan",
47 (Persona::Artisan, Lang::En) => "For a tradesperson",
48 (Persona::Maison, Lang::Fr) => "À la maison",
49 (Persona::Maison, Lang::En) => "At home",
50 (Persona::Builder, Lang::Fr) => "Pour un créateur",
51 (Persona::Builder, Lang::En) => "For a builder",
52 (Persona::Developpeur, Lang::Fr) => "Pour un développeur",
53 (Persona::Developpeur, Lang::En) => "For a developer",
54 (Persona::Expert, Lang::Fr) => "Pour un expert IA",
55 (Persona::Expert, Lang::En) => "For an AI expert",
56 }
57 }
58
59 fn from_slug(s: &str) -> Option<Persona> {
60 let s = s.trim().to_lowercase();
61 [
62 Persona::Enfant,
63 Persona::Enseignant,
64 Persona::GrandMere,
65 Persona::Artisan,
66 Persona::Maison,
67 Persona::Builder,
68 Persona::Developpeur,
69 Persona::Expert,
70 ]
71 .into_iter()
72 .find(|p| p.slug() == s || p.slug().starts_with(&s))
73 }
74}
75
76pub struct Recipe {
78 pub persona: Persona,
79 pub title_fr: &'static str,
80 pub title_en: &'static str,
81 pub prompt_fr: &'static str,
83 pub prompt_en: &'static str,
84 pub est: &'static str,
85}
86
87impl Recipe {
88 pub fn title(&self, lang: Lang) -> &'static str {
89 match lang {
90 Lang::Fr => self.title_fr,
91 Lang::En => self.title_en,
92 }
93 }
94 pub fn prompt(&self, lang: Lang) -> &'static str {
95 match lang {
96 Lang::Fr => self.prompt_fr,
97 Lang::En => self.prompt_en,
98 }
99 }
100}
101
102const RECIPES: &[Recipe] = &[
103 Recipe {
104 persona: Persona::Enseignant,
105 title_fr: "30 quiz à difficulté progressive depuis mon cours",
106 title_en: "30 progressive quizzes from my lesson",
107 prompt_fr: "Transforme le fichier de mon cours en 30 questions de quiz à difficulté croissante, avec les réponses à la fin.",
108 prompt_en: "Turn my lesson file into 30 quiz questions of increasing difficulty, with answers at the end.",
109 est: "~2 min",
110 },
111 Recipe {
112 persona: Persona::GrandMere,
113 title_fr: "Ranger mes photos par année et par personne",
114 title_en: "Sort my photos by year and by person",
115 prompt_fr: "Range les photos de ce dossier dans des sous-dossiers par année, et explique-moi chaque étape simplement.",
116 prompt_en: "Sort the photos in this folder into subfolders by year, and explain each step to me simply.",
117 est: "~5 min",
118 },
119 Recipe {
120 persona: Persona::Artisan,
121 title_fr: "Croiser mes factures Excel et mes bons de commande",
122 title_en: "Cross-check my Excel invoices against purchase orders",
123 prompt_fr: "Compare mon fichier de factures et mon fichier de bons de commande, et liste-moi tous les écarts de montant.",
124 prompt_en: "Compare my invoices file and my purchase-orders file, and list every amount mismatch.",
125 est: "~3 min",
126 },
127 Recipe {
128 persona: Persona::Enfant,
129 title_fr: "Pourquoi mon jeu Scratch plante ?",
130 title_en: "Why does my Scratch game crash?",
131 prompt_fr: "Explique-moi pourquoi mon projet plante, comme si j'avais 9 ans, et aide-moi à le réparer.",
132 prompt_en: "Explain why my project crashes, like I'm 9 years old, and help me fix it.",
133 est: "~2 min",
134 },
135 Recipe {
136 persona: Persona::Maison,
137 title_fr: "Un planning de repas de la semaine",
138 title_en: "A weekly meal plan",
139 prompt_fr: "Fais-moi un planning de repas équilibrés pour la semaine à partir de ma liste de courses, et la liste de ce qui manque.",
140 prompt_en: "Make me a balanced weekly meal plan from my shopping list, plus a list of what's missing.",
141 est: "~2 min",
142 },
143 Recipe {
144 persona: Persona::Builder,
145 title_fr: "Une page de présentation pour mon idée",
146 title_en: "A landing page for my idea",
147 prompt_fr: "Monte-moi une page web simple et jolie qui présente mon idée, prête à ouvrir dans un navigateur.",
148 prompt_en: "Build me a simple, good-looking web page that presents my idea, ready to open in a browser.",
149 est: "~10 min",
150 },
151 Recipe {
152 persona: Persona::Developpeur,
153 title_fr: "Trouver pourquoi ce test échoue 1 fois sur 20",
154 title_en: "Find why this test flakes 1 in 20 runs",
155 prompt_fr: "Ce test échoue de façon intermittente. Trouve la cause de l'instabilité et propose un correctif.",
156 prompt_en: "This test fails intermittently. Find the source of the flakiness and propose a fix.",
157 est: "~5 min",
158 },
159 Recipe {
160 persona: Persona::Developpeur,
161 title_fr: "Expliquer ce dépôt que je découvre",
162 title_en: "Explain this repo I'm new to",
163 prompt_fr: "Fais-moi le tour de ce dépôt : à quoi il sert, son architecture, et par où commencer pour contribuer.",
164 prompt_en: "Give me a tour of this repo: what it does, its architecture, and where to start contributing.",
165 est: "~3 min",
166 },
167 Recipe {
168 persona: Persona::Expert,
169 title_fr: "Orchestrer un swarm sur un gros monorepo",
170 title_en: "Orchestrate a swarm over a large monorepo",
171 prompt_fr: "Lance un swarm : un planner qui découpe la tâche, des coders en parallèle par module, un verifier qui teste chaque diff.",
172 prompt_en: "Run a swarm: a planner that splits the task, coders in parallel per module, a verifier that tests each diff.",
173 est: "variable",
174 },
175];
176
177pub fn search<'a>(persona: Option<&str>, query: Option<&str>) -> Vec<&'a Recipe> {
180 let persona = persona.and_then(Persona::from_slug);
181 let q = query
182 .map(|s| s.trim().to_lowercase())
183 .filter(|s| !s.is_empty());
184 RECIPES
185 .iter()
186 .filter(|r| persona.is_none_or(|p| r.persona == p))
187 .filter(|r| {
188 q.as_ref().is_none_or(|q| {
189 r.title_fr.to_lowercase().contains(q)
190 || r.title_en.to_lowercase().contains(q)
191 || r.prompt_fr.to_lowercase().contains(q)
192 || r.prompt_en.to_lowercase().contains(q)
193 })
194 })
195 .collect()
196}
197
198pub fn personas() -> Vec<&'static str> {
200 let mut seen = Vec::new();
201 for r in RECIPES {
202 if !seen.contains(&r.persona.slug()) {
203 seen.push(r.persona.slug());
204 }
205 }
206 seen
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn lists_everything_with_no_filter() {
215 assert_eq!(search(None, None).len(), RECIPES.len());
216 assert!(RECIPES.len() >= 8, "ship a meaningful starter gallery");
217 }
218
219 #[test]
220 fn filters_by_persona() {
221 let dev = search(Some("developpeur"), None);
222 assert!(!dev.is_empty());
223 assert!(dev.iter().all(|r| r.persona == Persona::Developpeur));
224 assert_eq!(search(Some("dev"), None).len(), dev.len());
226 }
227
228 #[test]
229 fn full_text_search_matches_title_and_prompt() {
230 assert!(!search(None, Some("photos")).is_empty());
231 assert!(!search(None, Some("swarm")).is_empty());
232 assert!(search(None, Some("zzzznotfound")).is_empty());
233 }
234
235 #[test]
236 fn personas_list_is_non_empty_and_unique() {
237 let p = personas();
238 assert!(!p.is_empty());
239 let mut sorted = p.clone();
240 sorted.sort_unstable();
241 sorted.dedup();
242 assert_eq!(sorted.len(), p.len(), "no duplicate personas");
243 }
244
245 #[test]
246 fn every_recipe_has_both_languages() {
247 for r in RECIPES {
248 assert!(!r.prompt(Lang::Fr).is_empty());
249 assert!(!r.prompt(Lang::En).is_empty());
250 assert!(!r.title(Lang::Fr).is_empty());
251 assert!(!r.title(Lang::En).is_empty());
252 }
253 }
254}