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 }; 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(); 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 ); 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(); 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}