flams_router_search/
components.rs

1use flams_ontology::{
2    search::{QueryFilter, SearchResult, SearchResultKind},
3    uris::{DocumentElementURI, DocumentURI, SymbolURI, URI},
4};
5use flams_router_base::uris::{DocURIComponents, URIComponents};
6use flams_utils::{impossible, vecmap::VecMap};
7use flams_web_utils::{components::error_with_toaster, inject_css};
8use leptos::prelude::*;
9
10#[derive(Debug, Clone)]
11pub(crate) enum SearchState {
12    None,
13    Loading,
14    Results(Vec<(f32, SearchResult)>),
15    SymResults(VecMap<SymbolURI, Vec<(f32, SearchResult)>>),
16}
17
18#[derive(Copy, Clone, PartialEq, Eq)]
19pub(crate) enum Filter {
20    Doc,
21    Def,
22    Par,
23    Ex,
24    Ass,
25}
26impl Filter {
27    const ALL: [Filter; 5] = [
28        Filter::Doc,
29        Filter::Def,
30        Filter::Par,
31        Filter::Ex,
32        Filter::Ass,
33    ];
34    fn from_value(s: &str) -> Self {
35        match s {
36            "doc" => Self::Doc,
37            "def" => Self::Def,
38            "par" => Self::Par,
39            "ex" => Self::Ex,
40            "ass" => Self::Ass,
41            _ => impossible!(),
42        }
43    }
44    fn value_str(self) -> &'static str {
45        match self {
46            Self::Doc => "doc",
47            Self::Def => "def",
48            Self::Par => "par",
49            Self::Ex => "ex",
50            Self::Ass => "ass",
51        }
52    }
53    fn tag_str(self) -> &'static str {
54        match self {
55            Self::Doc => "Documents",
56            Self::Def => "Definitions",
57            Self::Par => "Paragraphs",
58            Self::Ex => "Examples",
59            Self::Ass => "Assertions",
60        }
61    }
62    fn long_str(self) -> &'static str {
63        match self {
64            Self::Doc => "Full Documents",
65            Self::Def => "Definitions",
66            Self::Par => "Other Paragraphs",
67            Self::Ex => "(Counter-)examples",
68            Self::Ass => "Assertions (Theorems, Lemmata, etc.)",
69        }
70    }
71}
72
73#[component]
74pub fn SearchTop() -> impl IntoView {
75    use flams_web_utils::components::ClientOnly;
76    use thaw::{
77        Divider, Flex, FlexAlign, Icon, Input, InputPrefix, Layout, LayoutHeader, Radio,
78        RadioGroup, Tag, TagPicker, TagPickerControl, TagPickerGroup, TagPickerInput,
79        TagPickerOption, ToasterInjection,
80    }; //,Combobox,ComboboxOption
81    let query = RwSignal::new(String::new());
82    let search_kind = RwSignal::new(vec![
83        Filter::Def.value_str().to_string(),
84        Filter::Par.value_str().to_string(),
85    ]);
86    let query_opts = Memo::new(move |_| {
87        search_kind.with(|v| {
88            let mut ret = QueryFilter::default();
89            ret.allow_documents = false;
90            ret.allow_paragraphs = false;
91            ret.allow_definitions = false;
92            ret.allow_examples = false;
93            ret.allow_assertions = false;
94            ret.allow_problems = false;
95            for s in v {
96                match Filter::from_value(s.as_str()) {
97                    Filter::Doc => ret.allow_documents = true,
98                    Filter::Def => ret.allow_definitions = true,
99                    Filter::Par => ret.allow_paragraphs = true,
100                    Filter::Ex => ret.allow_examples = true,
101                    Filter::Ass => ret.allow_assertions = true,
102                }
103            }
104            ret
105        })
106    });
107    let results = RwSignal::new(SearchState::None);
108    let toaster = ToasterInjection::expect_context();
109    let action = Action::new(move |&()| {
110        results.set(SearchState::Loading);
111        let s = query.get_untracked();
112        let opts = query_opts.get_untracked();
113        async move {
114            match super::search_query(s, opts, 20).await {
115                Ok(r) => results.set(SearchState::Results(r)),
116                Err(e) => {
117                    results.set(SearchState::None);
118                    error_with_toaster(e, toaster);
119                }
120            }
121        }
122    });
123    let sym_action = Action::new(move |&()| {
124        results.set(SearchState::Loading);
125        let s = query.get_untracked();
126        async move {
127            match super::search_symbols(s, 20).await {
128                Ok(r) => results.set(SearchState::SymResults(r)),
129                Err(e) => {
130                    results.set(SearchState::None);
131                    error_with_toaster(e, toaster);
132                }
133            }
134        }
135    });
136    let radio_value = RwSignal::new("X".to_string());
137    Effect::new(move || {
138        if query.with(|q| q.is_empty()) {
139            return;
140        };
141        if radio_value.with(|s| s == "S") {
142            sym_action.dispatch(());
143        } else {
144            let _ = query_opts.get(); // register dependency
145            action.dispatch(());
146        }
147    });
148    inject_css(
149        "flams-search-picker",
150        ".flams-search-picker{} .flams-search-picker-disabled { display:none; }",
151    );
152    let cls = Memo::new(move |_| match radio_value.get().as_str() {
153        "X" => "flams-search-picker".to_string(),
154        "S" => "flams-search-picker-disabled".to_string(),
155        _ => impossible!(),
156    });
157    view! {
158      <Layout>
159        <LayoutHeader><Flex>
160          <Input value=query placeholder="search...">
161              <InputPrefix slot>
162                  <Icon icon=icondata_ai::AiSearchOutlined/>
163              </InputPrefix>
164          </Input>
165          <RadioGroup value=radio_value>
166            <Radio value="S" label="Symbols"/>
167            <Radio value="X" label="Documents/Paragraphs"/>
168          </RadioGroup>
169          <ClientOnly>
170            <TagPicker selected_options=search_kind class=cls>
171                <TagPickerControl slot>
172                <TagPickerGroup>
173                  {move ||
174                    search_kind.get().into_iter().map(|option| view!{
175                      <Tag value=option.clone() attr:style="background-color:var(--colorBrandBackground2)">
176                          {Filter::from_value(option.as_str()).tag_str()}
177                      </Tag>
178                    }).collect_view()
179                  }
180                  </TagPickerGroup>
181                  <TagPickerInput />
182                </TagPickerControl>
183                {
184                  move ||
185                      search_kind.with(|opts| {
186                          Filter::ALL.iter().filter_map(|option| {
187                              if opts.iter().any(|o| o == option.value_str()) {
188                                  return None
189                              } else {
190                                  Some(view! {
191                                      <TagPickerOption value=option.value_str().to_string() text=option.long_str() />
192                                  })
193                              }
194                          }).collect_view()
195                      })
196                }
197            </TagPicker>
198          </ClientOnly>
199        </Flex></LayoutHeader>
200        <Layout>
201          <Divider/>
202          <div style="width:fit-content;padding:10px;"><Flex vertical=true align=FlexAlign::Start>{move || do_results(results)}</Flex></div>
203        </Layout>
204      </Layout>
205    }
206}
207
208fn do_results(results: RwSignal<SearchState>) -> impl IntoView {
209    use leptos::either::EitherOf5::*;
210    results.with(|r| match r {
211        SearchState::None => A(()),
212        SearchState::Results(v) if v.is_empty() => B("(No results)"),
213        SearchState::Loading => C(view!(<flams_web_utils::components::Spinner/>)),
214        SearchState::SymResults(v) => D(v
215            .iter()
216            .map(|(sym, res)| do_sym_result(sym, res.clone()))
217            .collect_view()),
218        SearchState::Results(v) => E(v
219            .iter()
220            .map(|(score, res)| do_result(*score, res))
221            .collect_view()),
222    })
223}
224
225fn do_sym_result(sym: &SymbolURI, res: Vec<(f32, SearchResult)>) -> impl IntoView + use<> {
226    use flams_router_content::components::Fragment;
227    use flams_web_utils::components::ClientOnly;
228    use thaw::{Body1, Card, CardHeader, CardPreview, Scrollbar};
229
230    let name = ftml_viewer_components::components::omdoc::symbol_name(sym, &sym.to_string());
231    view! {
232      <Card>
233          <CardHeader>
234              <Body1><b>{name}</b></Body1>
235          </CardHeader>
236          <CardPreview>
237            <div style="padding:0 5px;max-width:100%">
238              <div style="width:100%;color:black;background-color:white;">
239                <Scrollbar style="max-height: 100px;width:100%;max-width:100%;">{
240                  res.into_iter().map(|(_,r)| {
241                    let SearchResult::Paragraph { uri, .. } = r else { impossible!()};
242                    view!{
243                        //<span>"Here: "{uri.to_string()}</span>
244                        //<div>"---"</div>
245                        <Fragment uri=URIComponents::Uri(URI::Narrative(uri.into())) />
246                        //<div>"---"</div>
247                    }
248                  }).collect_view()
249                }
250                </Scrollbar>
251              </div>
252            </div>
253          </CardPreview>
254      </Card>
255    }
256}
257
258fn do_result(score: f32, res: &SearchResult) -> impl IntoView + use<> {
259    use leptos::either::Either::*;
260    match res {
261        SearchResult::Document(d) => Left(do_doc(score, d.clone())),
262        SearchResult::Paragraph {
263            uri, fors, kind, ..
264        } => Right(do_para(score, uri.clone(), *kind, fors.clone())),
265    }
266}
267
268fn do_doc(score: f32, uri: DocumentURI) -> impl IntoView {
269    use flams_router_content::components::DocumentInner;
270    use ftml_viewer_components::components::omdoc::doc_name;
271    use thaw::{Body1, Card, CardHeader, CardHeaderAction, CardPreview, Scrollbar};
272    let name = doc_name(&uri, uri.name().to_string());
273    view! {
274      <Card>
275          <CardHeader>
276              <Body1>
277                  <b>"Document "{name}</b>
278              </Body1>
279              /*<CardHeaderDescription slot>
280                  <Caption1>"Description"</Caption1>
281              </CardHeaderDescription>*/
282              <CardHeaderAction slot>
283                  <span>"Score: "{score}</span>
284              </CardHeaderAction>
285          </CardHeader>
286          <CardPreview>
287              <div style="padding:0 5px;max-width:100%">
288                <div style="width:100%;color:black;background-color:white;">
289                    <Scrollbar style="max-height: 100px;;width:100%;max-width:100%;"><DocumentInner doc=DocURIComponents::Uri(uri) /></Scrollbar>
290                </div>
291              </div>
292          </CardPreview>
293          /*<CardFooter>
294              "sTeX:"<pre></pre>
295          </CardFooter>*/
296      </Card>
297    }
298}
299
300fn do_para(
301    score: f32,
302    uri: DocumentElementURI,
303    kind: SearchResultKind,
304    fors: Vec<SymbolURI>,
305) -> impl IntoView {
306    use flams_router_content::components::Fragment;
307    use flams_web_utils::components::{Popover, PopoverTrigger};
308    use ftml_viewer_components::components::omdoc::{comma_sep, symbol_name};
309    use thaw::{
310        Body1, Caption1, Card, CardHeader, CardHeaderAction, CardHeaderDescription, CardPreview,
311        Scrollbar,
312    };
313    let uristr = uri.to_string();
314    let namestr = uri.name().to_string();
315    let name = view! {
316      <div style="display:inline-block;"><Popover>
317      <PopoverTrigger slot><span class="ftml-comp">{namestr}</span></PopoverTrigger>
318      <div style="font-size:small;">{uristr}</div>
319      </Popover></div>
320    };
321    let desc = comma_sep(
322        "For",
323        fors.into_iter()
324            .map(|s| symbol_name(&s, s.name().last_name().as_ref())),
325    );
326    view! {
327      <Card>
328          <CardHeader>
329              <Body1>
330                  <b>{kind.as_str()}" "{name}</b>
331              </Body1>
332              <CardHeaderDescription slot>
333                  <Caption1>{desc}</Caption1>
334              </CardHeaderDescription>
335              <CardHeaderAction slot>
336                  <span>"Score: "{score}</span>
337              </CardHeaderAction>
338          </CardHeader>
339          <CardPreview>
340            <div style="padding:0 5px;max-width:100%">
341              <div style="width:100%;color:black;background-color:white;">
342                <Scrollbar style="max-height: 100px;width:100%;max-width:100%;"><Fragment uri=URIComponents::Uri(URI::Narrative(uri.into())) /></Scrollbar>
343              </div>
344            </div>
345          </CardPreview>
346          /*<CardFooter>
347              "sTeX:"<pre></pre>
348          </CardFooter>*/
349      </Card>
350    }
351}