module Gargantext.Components.NgramsTable ( MainNgramsTableProps , CommonProps , mainNgramsTable ) where import Data.Array as A import Data.FunctorWithIndex (mapWithIndex) import Data.Lens (Lens', to, (%~), (.~), (^.), (^?), view) import Data.Lens.At (at) import Data.Lens.Common (_Just) import Data.Lens.Fold (folded) import Data.Lens.Index (ix) import Data.Lens.Record (prop) import Data.Map (Map) import Data.Map as Map import Data.Maybe (Maybe(..), isNothing, maybe) import Data.Monoid.Additive (Additive(..)) import Data.Ord.Down (Down(..)) import Data.Sequence (Seq, length) as Seq import Data.Set (Set) import Data.Set as Set import Data.Symbol (SProxy(..)) import Data.Tuple (Tuple(..), fst, snd) import Data.Tuple.Nested ((/\)) import DOM.Simple.Console (log, log2) import Effect (Effect) import Effect.Aff (Aff, launchAff_) import Effect.Class (liftEffect) import Reactix as R import Reactix.DOM.HTML as H import Record as Record import Unsafe.Coerce (unsafeCoerce) import Gargantext.Prelude import Gargantext.AsyncTasks as GAT import Gargantext.Components.AutoUpdate (autoUpdateElt) import Gargantext.Hooks.Loader (useLoader) import Gargantext.Components.NgramsTable.Components as NTC import Gargantext.Components.NgramsTable.Core import Gargantext.Components.NgramsTable.Loader (useLoaderWithCacheAPI) import Gargantext.Components.Nodes.Lists.Types as NT import Gargantext.Components.Table as T import Gargantext.Routes (SessionRoute(..)) as R import Gargantext.Sessions (Session, get) import Gargantext.Types (CTabNgramType, OrderBy(..), ReloadS, SearchQuery, TabType, TermList(..), TermSize, termLists, termSizes) import Gargantext.Utils (queryMatchesLabel, toggleSet, sortWith) import Gargantext.Utils.CacheAPI as GUC import Gargantext.Utils.Reactix as R2 import Gargantext.Utils.Seq (mapMaybe) as Seq thisModule :: String thisModule = "Gargantext.Components.NgramsTable" type State' = CoreState ( ngramsParent :: Maybe NgramsTerm -- Nothing means we are not currently grouping terms , ngramsChildren :: Map NgramsTerm Boolean -- ^ Used only when grouping. -- This updates the children of `ngramsParent`, -- ngrams set to `true` are to be added, and `false` to -- be removed. , ngramsSelection :: Set NgramsTerm -- ^ The set of selected checkboxes of the first column. ) _ngramsChildren :: forall row. Lens' { ngramsChildren :: Map NgramsTerm Boolean | row } (Map NgramsTerm Boolean) _ngramsChildren = prop (SProxy :: SProxy "ngramsChildren") _ngramsSelection :: forall row. Lens' { ngramsSelection :: Set NgramsTerm | row } (Set NgramsTerm) _ngramsSelection = prop (SProxy :: SProxy "ngramsSelection") initialState' :: VersionedNgramsTable -> State' initialState' (Versioned {version}) = { ngramsChildren: mempty , ngramsLocalPatch: mempty , ngramsParent: Nothing , ngramsSelection: mempty , ngramsStagePatch: mempty , ngramsValidPatch: mempty , ngramsVersion: version } type State = CoreState ( ngramsChildren :: Map NgramsTerm Boolean -- ^ Used only when grouping. -- This updates the children of `ngramsParent`, -- ngrams set to `true` are to be added, and `false` to -- be removed. , ngramsParent :: Maybe NgramsTerm -- Nothing means we are not currently grouping terms , ngramsSelection :: Set NgramsTerm -- ^ The set of selected checkboxes of the first column. ) initialState :: VersionedNgramsTable -> State initialState (Versioned {version}) = { ngramsChildren: mempty , ngramsLocalPatch: mempty , ngramsParent: Nothing , ngramsSelection: mempty , ngramsStagePatch: mempty , ngramsValidPatch: mempty , ngramsVersion: version } setTermListSetA :: NgramsTable -> Set NgramsTerm -> TermList -> Action setTermListSetA ngramsTable ns new_list = CoreAction $ CommitPatch $ fromNgramsPatches $ PatchMap $ mapWithIndex f $ toMap ns where f :: NgramsTerm -> Unit -> NgramsPatch f n unit = NgramsPatch { patch_list, patch_children: mempty } where cur_list = ngramsTable ^? at n <<< _Just <<< _NgramsRepoElement <<< _list patch_list = maybe mempty (\c -> replace c new_list) cur_list toMap :: forall a. Set a -> Map a Unit toMap = unsafeCoerce -- TODO https://github.com/purescript/purescript-ordered-collections/pull/21 -- https://github.com/purescript/purescript-ordered-collections/pull/31 -- toMap = Map.fromFoldable type PreConversionRows = Seq.Seq NgramsElement type TableContainerProps = ( dispatch :: Dispatch , ngramsChildren :: Map NgramsTerm Boolean , ngramsParent :: Maybe NgramsTerm , ngramsSelection :: Set NgramsTerm , ngramsTable :: NgramsTable , path :: R.State PageParams , tabNgramType :: CTabNgramType ) tableContainer :: Record TableContainerProps -> Record T.TableContainerProps -> R.Element tableContainer p q = R.createElement (tableContainerCpt p) q [] tableContainerCpt :: Record TableContainerProps -> R.Component T.TableContainerProps tableContainerCpt { dispatch , ngramsChildren , ngramsParent , ngramsSelection , ngramsTable: ngramsTableCache , path: {searchQuery, termListFilter, termSizeFilter} /\ setPath , tabNgramType } = R.hooksComponentWithModule thisModule "tableContainer" cpt where cpt props _ = do pure $ H.div {className: "container-fluid"} [ H.div {className: "jumbotron1"} [ R2.row [ H.div {className: "panel panel-default"} [ H.div {className: "panel-heading"} [ R2.row [ H.div {className: "col-md-2", style: {marginTop: "6px"}} [ if A.null props.tableBody && searchQuery /= "" then H.li { className: "list-group-item" } [ H.button { className: "btn btn-primary" , on: { click: const $ dispatch $ CoreAction $ addNewNgramA (normNgram tabNgramType searchQuery) CandidateTerm } } [ H.text ("Add " <> searchQuery) ] ] else H.div {} [] ] , H.div {className: "col-md-2", style: {marginTop : "6px"}} [ H.li {className: "list-group-item"} [ R2.select { id: "picklistmenu" , className: "form-control custom-select" , defaultValue: (maybe "" show termListFilter) , on: {change: setTermListFilter <<< read <<< R.unsafeEventValue}} (map optps1 termLists)] ] , H.div {className: "col-md-2", style: {marginTop : "6px"}} [ H.li {className: "list-group-item"} [ R2.select {id: "picktermtype" , className: "form-control custom-select" , defaultValue: (maybe "" show termSizeFilter) , on: {change: setTermSizeFilter <<< read <<< R.unsafeEventValue}} (map optps1 termSizes)] ] , H.div { className: "col-md-2", style: { marginTop: "6px" } } [ H.li {className: "list-group-item"} [ H.div { className: "form-inline" } [ H.div { className: "form-group" } [ props.pageSizeControl , H.label {} [ H.text " items" ] -- H.div { className: "col-md-6" } [ props.pageSizeControl ] -- , H.div { className: "col-md-6" } [ -- ] ] ] ] ] , H.div {className: "col-md-4", style: {marginTop : "6px", marginBottom : "1px"}} [ H.li {className: "list-group-item"} [ props.pageSizeDescription , props.paginationLinks ] ] ] ] , editor , if (selectionsExist ngramsSelection) then H.li {className: "list-group-item"} [ selectButtons true ] else H.div {} [] , H.div {id: "terms_table", className: "panel-body"} [ H.table {className: "table able"} [ H.thead {className: "tableHeader"} [props.tableHead] , H.tbody {} props.tableBody ] , H.li {className: "list-group-item"} [ H.div { className: "row" } [ H.div { className: "col-md-4" } [ selectButtons (selectionsExist ngramsSelection) ] , H.div { className: "col-md-4 col-md-offset-4" } [ props.paginationLinks ] ] ] ] ] ] ] ] -- WHY setPath f = origSetPageParams (const $ f path) setTermListFilter x = setPath $ _ { termListFilter = x } setTermSizeFilter x = setPath $ _ { termSizeFilter = x } setSelection = dispatch <<< setTermListSetA ngramsTableCache ngramsSelection editor = H.div {} $ maybe [] f ngramsParent where f ngrams = [ H.p {} [H.text $ "Editing " <> ngramsTermText ngrams] , NTC.renderNgramsTree { ngramsTable , ngrams , ngramsStyle: [] , ngramsClick , ngramsEdit } , H.button { className: "btn btn-primary" , on: {click: (const $ dispatch AddTermChildren)} } [H.text "Save"] , H.button { className: "btn btn-secondary" , on: {click: (const $ dispatch $ SetParentResetChildren Nothing)} } [H.text "Cancel"] ] where ngramsTable = ngramsTableCache # at ngrams <<< _Just <<< _NgramsRepoElement <<< _children %~ applyPatchSet (patchSetFromMap ngramsChildren) ngramsClick {depth: 1, ngrams: child} = Just $ dispatch $ ToggleChild false child ngramsClick _ = Nothing ngramsEdit _ = Nothing selectionsExist :: Set NgramsTerm -> Boolean selectionsExist = not <<< Set.isEmpty selectButtons false = H.div {} [] selectButtons true = H.div {} [ H.button { className: "btn btn-primary" , on: { click: const $ setSelection MapTerm } } [ H.text "Map" ] , H.button { className: "btn btn-primary" , on: { click: const $ setSelection StopTerm } } [ H.text "Stop" ] , H.button { className: "btn btn-primary" , on: { click: const $ setSelection CandidateTerm } } [ H.text "Candidate" ] ] -- NEXT type CommonProps = ( afterSync :: Unit -> Aff Unit , appReload :: ReloadS , asyncTasksRef :: R.Ref (Maybe GAT.Reductor) , sidePanelTriggers :: Record NT.SidePanelTriggers , tabNgramType :: CTabNgramType , treeReloadRef :: R.Ref (Maybe ReloadS) , withAutoUpdate :: Boolean ) type Props = ( path :: R.State PageParams , state :: R.State State , versioned :: VersionedNgramsTable | CommonProps ) loadedNgramsTable :: Record Props -> R.Element loadedNgramsTable p = R.createElement loadedNgramsTableCpt p [] loadedNgramsTableCpt :: R.Component Props loadedNgramsTableCpt = R.hooksComponentWithModule thisModule "loadedNgramsTable" cpt where cpt { afterSync , appReload , asyncTasksRef , path: path@(path'@{ listIds, nodeId, params, searchQuery, scoreType, termListFilter, termSizeFilter } /\ setPath) , sidePanelTriggers , state: (state@{ ngramsChildren , ngramsLocalPatch , ngramsParent , ngramsSelection , ngramsVersion } /\ setState) , tabNgramType , treeReloadRef , versioned: Versioned { data: initTable } , withAutoUpdate } _ = do let syncResetBtns = [ syncResetButtons { afterSync: chartsAfterSync , ngramsLocalPatch , performAction: performAction <<< CoreAction } ] pure $ R.fragment $ autoUpdate <> syncResetBtns <> [ H.h4 {style: {textAlign : "center"}} [ H.span {className: "glyphicon glyphicon-hand-down"} [] , H.text "Extracted Terms" ] , search , T.table { colNames , container: tableContainer { dispatch: performAction , ngramsChildren , ngramsParent , ngramsSelection , ngramsTable , path , tabNgramType } , params: params /\ setParams -- TODO-LENS , rows: filteredConvertedRows , totalRecords , wrapColElts: wrapColElts { allNgramsSelected , dispatch: performAction , ngramsSelection } } ] <> syncResetBtns where chartsAfterSync _ = do task <- postNgramsChartsAsync path' liftEffect $ do log2 "[chartsAfterSync] Synchronize task" task case R.readRef asyncTasksRef of Nothing -> log "[chartsAfterSync] asyncTasksRef is Nothing" Just asyncTasks -> do snd asyncTasks $ GAT.Insert nodeId task case R.readRef treeReloadRef of Nothing -> log "[chartsAfterSync] can't reload tree: ref empty" Just treeReload -> do snd treeReload $ (_ + 1) -- snd appReload $ (_ + 1) autoUpdate :: Array R.Element autoUpdate = if withAutoUpdate then [ R2.buff $ autoUpdateElt { duration: 5000 , effect: performAction $ CoreAction $ Synchronize { afterSync: chartsAfterSync } } ] else [] setParentResetChildren :: Maybe NgramsTerm -> State -> State setParentResetChildren p = _ { ngramsParent = p, ngramsChildren = mempty } performAction :: Action -> Effect Unit performAction (SetParentResetChildren p) = setState $ setParentResetChildren p performAction (ToggleChild b c) = setState $ \s@{ ngramsChildren: nc } -> s { ngramsChildren = newNC nc } where newNC nc = Map.alter (maybe (Just b) (const Nothing)) c nc performAction (ToggleSelect c) = setState $ \s@{ ngramsSelection: ns } -> s { ngramsSelection = toggleSet c ns } performAction ToggleSelectAll = setState toggler where toggler s = if allNgramsSelected then s { ngramsSelection = Set.empty :: Set NgramsTerm } else s { ngramsSelection = selectNgramsOnFirstPage filteredRows } performAction AddTermChildren = case ngramsParent of Nothing -> -- impossible but harmless pure unit Just parent -> do let pc = patchSetFromMap ngramsChildren pe = NgramsPatch { patch_list: mempty, patch_children: pc } pt = singletonNgramsTablePatch parent pe setState $ setParentResetChildren Nothing commitPatch (Versioned {version: ngramsVersion, data: pt}) (state /\ setState) performAction (CoreAction a) = coreDispatch path' (state /\ setState) a totalRecords = Seq.length rows filteredConvertedRows :: T.Rows filteredConvertedRows = convertRow <$> filteredRows filteredRows :: PreConversionRows filteredRows = T.filterRows { params } rows ng_scores :: Map NgramsTerm (Additive Int) ng_scores = ngramsTable ^. _NgramsTable <<< _ngrams_scores rows :: PreConversionRows rows = orderWith ( Seq.mapMaybe (\(Tuple ng nre) -> let Additive s = ng_scores ^. at ng <<< _Just in addOcc <$> rowsFilter (ngramsRepoElementToNgramsElement ng s nre)) $ Map.toUnfoldable (ngramsTable ^. _NgramsTable <<< _ngrams_repo_elements) ) rowsFilter :: NgramsElement -> Maybe NgramsElement rowsFilter ne = if displayRow state searchQuery ngramsTable ngramsParentRoot termListFilter termSizeFilter ne then Just ne else Nothing addOcc ngramsElement = let Additive occurrences = sumOccurrences ngramsTable (ngramsElementToNgramsOcc ngramsElement) in ngramsElement # _NgramsElement <<< _occurrences .~ occurrences allNgramsSelected = allNgramsSelectedOnFirstPage ngramsSelection filteredRows ngramsTable = applyNgramsPatches state initTable roots = rootsOf ngramsTable ngramsParentRoot :: Maybe NgramsTerm ngramsParentRoot = (\np -> ngramsTable ^? at np <<< _Just <<< _NgramsRepoElement <<< _root <<< _Just ) =<< ngramsParent convertRow ngramsElement = { row: NTC.renderNgramsItem { dispatch: performAction , ngrams: ngramsElement ^. _NgramsElement <<< _ngrams , ngramsElement , ngramsLocalPatch , ngramsParent , ngramsSelection , ngramsTable , sidePanelTriggers } [] , delete: false } orderWith = case convOrderBy <$> params.orderBy of Just ScoreAsc -> sortWith \x -> x ^. _NgramsElement <<< _occurrences Just ScoreDesc -> sortWith \x -> Down $ x ^. _NgramsElement <<< _occurrences Just TermAsc -> sortWith \x -> x ^. _NgramsElement <<< _ngrams Just TermDesc -> sortWith \x -> Down $ x ^. _NgramsElement <<< _ngrams _ -> identity -- the server ordering is enough here colNames = T.ColumnName <$> ["Show", "Select", "Map", "Stop", "Terms", "Score"] -- see convOrderBy -- This is used to *decorate* the Select header with the checkbox. wrapColElts scProps (T.ColumnName "Select") = const [NTC.selectionCheckbox scProps] wrapColElts _ (T.ColumnName "Score") = (_ <> [H.text ("(" <> show scoreType <> ")")]) wrapColElts _ _ = identity setParams f = setPath $ \p@{params: ps} -> p {params = f ps} search :: R.Element search = NTC.searchInput { key: "search-input" , onSearch: setSearchQuery , searchQuery: searchQuery } setSearchQuery :: String -> Effect Unit setSearchQuery x = setPath $ _ { searchQuery = x } displayRow :: State -> SearchQuery -> NgramsTable -> Maybe NgramsTerm -> Maybe TermList -> Maybe TermSize -> NgramsElement -> Boolean displayRow state@{ ngramsChildren , ngramsLocalPatch , ngramsParent } searchQuery ngramsTable ngramsParentRoot termListFilter termSizeFilter (NgramsElement {ngrams, root, list}) = ( isNothing root -- ^ Display only nodes without parents && maybe true (_ == list) termListFilter -- ^ and which matches the ListType filter. && ngramsChildren ^. at ngrams /= Just true -- ^ and which are not scheduled to be added already && Just ngrams /= ngramsParent -- ^ and which are not our new parent && Just ngrams /= ngramsParentRoot -- ^ and which are not the root of our new parent && filterTermSize termSizeFilter ngrams -- ^ and which satisfies the chosen term size || ngramsChildren ^. at ngrams == Just false -- ^ unless they are scheduled to be removed. || NTC.tablePatchHasNgrams ngramsLocalPatch ngrams -- ^ unless they are being processed at the moment. ) && queryMatchesLabel searchQuery (ngramsTermText ngrams) -- ^ and which matches the search query. allNgramsSelectedOnFirstPage :: Set NgramsTerm -> PreConversionRows -> Boolean allNgramsSelectedOnFirstPage selected rows = selected == (selectNgramsOnFirstPage rows) selectNgramsOnFirstPage :: PreConversionRows -> Set NgramsTerm selectNgramsOnFirstPage rows = Set.fromFoldable $ (view $ _NgramsElement <<< _ngrams) <$> rows type MainNgramsTableProps = ( cacheState :: R.State NT.CacheState , defaultListId :: Int , nodeId :: Int -- ^ This node can be a corpus or contact. , pathS :: R.State PageParams , session :: Session , tabType :: TabType | CommonProps ) mainNgramsTable :: Record MainNgramsTableProps -> R.Element mainNgramsTable props = R.createElement mainNgramsTableCpt props [] mainNgramsTableCpt :: R.Component MainNgramsTableProps mainNgramsTableCpt = R.hooksComponentWithModule thisModule "mainNgramsTable" cpt where cpt props@{ afterSync , appReload , asyncTasksRef , cacheState , defaultListId , nodeId , pathS , session , sidePanelTriggers , tabNgramType , tabType , treeReloadRef , withAutoUpdate } _ = do -- let path = initialPageParams session nodeId [defaultListId] tabType case cacheState of (NT.CacheOn /\ _) -> do let render versioned = mainNgramsTablePaint { afterSync , appReload , asyncTasksRef , path: fst pathS , sidePanelTriggers , tabNgramType , treeReloadRef , versioned , withAutoUpdate } useLoaderWithCacheAPI { cacheEndpoint: versionEndpoint props , handleResponse , mkRequest , path: fst pathS , renderer: render } (NT.CacheOff /\ _) -> do -- pathS <- R.useState' path let render versioned = mainNgramsTablePaintNoCache { afterSync , appReload , asyncTasksRef , pathS , sidePanelTriggers , tabNgramType , treeReloadRef , versioned , withAutoUpdate } useLoader (fst pathS) loader render versionEndpoint :: Record MainNgramsTableProps -> PageParams -> Aff Version versionEndpoint { defaultListId, nodeId, session, tabType } _ = get session $ R.GetNgramsTableVersion { listId: defaultListId, tabType } (Just nodeId) -- NOTE With cache off loader :: PageParams -> Aff VersionedNgramsTable loader path@{ listIds , nodeId , params: { limit, offset, orderBy } , searchQuery , session , tabType , termListFilter , termSizeFilter } = get session $ R.GetNgrams params (Just nodeId) where params = { limit , listIds , offset: Just offset , orderBy: Nothing -- TODO , searchQuery , tabType , termListFilter , termSizeFilter } -- NOTE With cache on mkRequest :: PageParams -> GUC.Request mkRequest path@{ session } = GUC.makeGetRequest session $ url path where url { listIds , nodeId , params: { limit, offset, orderBy } , searchQuery , scoreType , tabType , termListFilter , termSizeFilter } = R.GetNgramsTableAll { listIds , tabType } (Just nodeId) handleResponse :: VersionedNgramsTable -> VersionedNgramsTable handleResponse v = v type MainNgramsTablePaintProps = ( path :: PageParams , versioned :: VersionedNgramsTable | CommonProps ) mainNgramsTablePaint :: Record MainNgramsTablePaintProps -> R.Element mainNgramsTablePaint p = R.createElement mainNgramsTablePaintCpt p [] mainNgramsTablePaintCpt :: R.Component MainNgramsTablePaintProps mainNgramsTablePaintCpt = R.hooksComponentWithModule thisModule "mainNgramsTablePaint" cpt where cpt props@{ afterSync , appReload , asyncTasksRef , path , sidePanelTriggers , tabNgramType , treeReloadRef , versioned , withAutoUpdate } _ = do pathS <- R.useState' path state <- R.useState' $ initialState versioned pure $ loadedNgramsTable { afterSync , appReload , asyncTasksRef , path: pathS , sidePanelTriggers , state , tabNgramType , treeReloadRef , versioned , withAutoUpdate } type MainNgramsTablePaintNoCacheProps = ( pathS :: R.State PageParams , versioned :: VersionedNgramsTable | CommonProps ) mainNgramsTablePaintNoCache :: Record MainNgramsTablePaintNoCacheProps -> R.Element mainNgramsTablePaintNoCache p = R.createElement mainNgramsTablePaintNoCacheCpt p [] mainNgramsTablePaintNoCacheCpt :: R.Component MainNgramsTablePaintNoCacheProps mainNgramsTablePaintNoCacheCpt = R.hooksComponentWithModule thisModule "mainNgramsTablePaintNoCache" cpt where cpt props@{ afterSync , appReload , asyncTasksRef , pathS , sidePanelTriggers , tabNgramType , treeReloadRef , versioned , withAutoUpdate } _ = do state <- R.useState' $ initialState versioned pure $ loadedNgramsTable { afterSync , appReload , asyncTasksRef , path: pathS , sidePanelTriggers , state , tabNgramType , treeReloadRef , versioned , withAutoUpdate } -- type MainNgramsTablePaintWithStateProps = ( -- afterSync :: Unit -> Aff Unit -- , asyncTasksRef :: R.Ref (Maybe GAT.Reductor) -- , path :: R.State PageParams -- , tabNgramType :: CTabNgramType -- , versioned :: VersionedNgramsTable -- , withAutoUpdate :: Boolean -- ) -- mainNgramsTablePaintWithState :: Record MainNgramsTablePaintWithStateProps -> R.Element -- mainNgramsTablePaintWithState p = R.createElement mainNgramsTablePaintWithStateCpt p [] -- mainNgramsTablePaintWithStateCpt :: R.Component MainNgramsTablePaintWithStateProps -- mainNgramsTablePaintWithStateCpt = R.hooksComponentWithModule thisModule "mainNgramsTablePaintWithState" cpt -- where -- cpt { afterSync, asyncTasksRef, path, tabNgramType, versioned, withAutoUpdate } _ = do -- state <- R.useState' $ initialState versioned -- pure $ loadedNgramsTable { -- afterSync -- , asyncTasksRef -- , path -- , state -- , tabNgramType -- , versioned -- , withAutoUpdate -- } type NgramsOcc = { occurrences :: Additive Int, children :: Set NgramsTerm } ngramsElementToNgramsOcc :: NgramsElement -> NgramsOcc ngramsElementToNgramsOcc (NgramsElement {occurrences, children}) = {occurrences: Additive occurrences, children} sumOccurrences :: NgramsTable -> NgramsOcc -> Additive Int sumOccurrences nt = sumOccChildren mempty where sumOccTerm :: Set NgramsTerm -> NgramsTerm -> Additive Int sumOccTerm seen label | Set.member label seen = Additive 0 -- TODO: Should not happen, emit a warning/error. | otherwise = sumOccChildren (Set.insert label seen) { occurrences: nt ^. _NgramsTable <<< _ngrams_scores <<< ix label , children: nt ^. ix label <<< _NgramsRepoElement <<< _children } sumOccChildren :: Set NgramsTerm -> NgramsOcc -> Additive Int sumOccChildren seen {occurrences, children} = occurrences <> children ^. folded <<< to (sumOccTerm seen) optps1 :: forall a. Show a => { desc :: String, mval :: Maybe a } -> R.Element optps1 { desc, mval } = H.option { value: value } [H.text desc] where value = maybe "" show mval