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 }; 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(); 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 <Fragment uri=URIComponents::Uri(URI::Narrative(uri.into())) />
246 }
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 <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 </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 </Card>
350 }
351}