Skip to main content

flams_router_search/
components.rs

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