Browse Source

refactor(srs): update some basic ui

rcmerci 1 year ago
parent
commit
ef2f79e2a1

+ 1 - 1
deps/db/src/logseq/db/frontend/class.cljs

@@ -17,7 +17,7 @@
 
    :logseq.class/Card
    {:title "Card"
-    :schema {:properties [:logseq.property/fsrs-state]}}
+    :schema {:properties [:logseq.property.fsrs/state :logseq.property.fsrs/due]}}
 
    :logseq.class/Journal {:title "Journal"
                           :properties {:logseq.property.journal/title-format "MMM do, yyyy"}}

+ 8 - 4
deps/db/src/logseq/db/frontend/property.cljs

@@ -223,10 +223,14 @@
                                            {:type :map
                                             :hide? true
                                             :public? false}}
-   :logseq.property/fsrs-state {:schema
+   :logseq.property.fsrs/state {:schema
                                 {:type :map
-                                 :hide? false ; show for debug now, hide it later
-                                 :public? false}}))
+                                 :hide? false ; TODO: show for debug now, hide it later
+                                 :public? false}}
+   :logseq.property.fsrs/due {:schema
+                              {:type :number ;TODO: change to :datetime when supported
+                               :hide? false
+                               :public? false}}))
 
 (def built-in-properties
   (->> built-in-properties*
@@ -253,7 +257,7 @@
         "All db attribute properties are configured in built-in-properties")
 
 (def logseq-property-namespaces
-  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.task"
+  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs" "logseq.task"
     "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table"
     "logseq.property.journal" "logseq.property.class" "logseq.property.view"})
 

+ 1 - 1
src/main/frontend/components/container.cljs

@@ -370,7 +370,7 @@
                  :icon-extension? true
                  :shortcut :go/whiteboards})))
 
-           (when (and (state/enable-flashcards? (state/get-current-repo)) (not db-based?))
+           (when (state/enable-flashcards? (state/get-current-repo))
              [:div.flashcards-nav
               (flashcards srs-open?)])
 

+ 127 - 14
src/main/frontend/extensions/fsrs.cljs

@@ -1,39 +1,152 @@
 (ns frontend.extensions.fsrs
-  (:require [frontend.db :as db]
-            [frontend.handler.property :as property-handler]
+  "Flashcards functions based on FSRS, only works in db-based graphs"
+  (:require [datascript.core :as d]
+            [frontend.components.block :as component-block]
+            [frontend.context.i18n :refer [t]]
+            [frontend.db :as db]
+            [frontend.handler.db-based.property :as db-property-handler]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
             [open-spaced-repetition.cljc-fsrs.core :as fsrs.core]
+            [rum.core :as rum]
             [tick.core :as tick]))
 
+(def ^:private instant->inst-ms (comp inst-ms tick/inst))
+(defn- inst-ms->instant [ms] (tick/instant (js/Date. ms)))
+
 (defn- fsrs-card-map->property-fsrs-state
   "Convert card-map to value stored in property"
   [fsrs-card-map]
   (-> fsrs-card-map
-      (update :last-repeat tick/inst)
-      (update :due tick/inst)))
+      (update :last-repeat instant->inst-ms)
+      (update :due instant->inst-ms)))
 
 (defn- property-fsrs-state->fsrs-card-map
   "opposite version of `fsrs-card->property-fsrs-state`"
   [prop-fsrs-state]
   (-> prop-fsrs-state
-      (update :last-repeat tick/instant)
-      (update :due tick/instant)))
+      (update :last-repeat inst-ms->instant)
+      (update :due inst-ms->instant)))
 
 (defn- get-card-map
   "Return nil if block is not #card.
-  Return default card-map if :logseq.property/fsrs-state is nil"
+  Return default card-map if `:logseq.property.fsrs/state` or `:logseq.property.fsrs/due` is nil"
   [block-entity]
   (when (some (fn [tag] (= :logseq.class/Card (:db/ident tag))) ;block should contains #Card
               (:block/tags block-entity))
-    (or (some-> (:logseq.property/fsrs-state block-entity)
-                property-fsrs-state->fsrs-card-map)
-        (fsrs.core/new-card!))))
+    (let [fsrs-state (:logseq.property.fsrs/state block-entity)
+          fsrs-due (:property.value/content (:logseq.property.fsrs/due block-entity))
+          return-default-card-map? (not (and fsrs-state fsrs-due))]
+      (if return-default-card-map?
+        (fsrs.core/new-card!)
+        (property-fsrs-state->fsrs-card-map (assoc fsrs-state :due fsrs-due))))))
 
 (defn repeat-card!
   [repo block-id rating]
   (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)
         block-entity (db/entity repo eid)]
     (when-let [card-map (get-card-map block-entity)]
-      (let [next-card-map (fsrs.core/repeat-card! card-map rating)]
-        (property-handler/set-block-property!
-         repo block-id
-         :logseq.property/fsrs-state (fsrs-card-map->property-fsrs-state next-card-map))))))
+      (let [next-card-map (fsrs.core/repeat-card! card-map rating)
+            prop-card-map (fsrs-card-map->property-fsrs-state next-card-map)
+            prop-fsrs-state (dissoc prop-card-map :due)
+            prop-fsrs-due (:due prop-card-map)]
+        (db-property-handler/set-block-property!
+         block-id :logseq.property.fsrs/state prop-fsrs-state)
+        (db-property-handler/create-property-text-block!
+         block-id :logseq.property.fsrs/due (str prop-fsrs-due)
+         {:new-block-id (db/new-block-id)})))))
+
+(defn get-card-block-ids
+  [repo]
+  (let [db (db/get-db repo)]
+    (->>
+     (d/q '[:find ?b
+            :where
+            [?b :block/tags :logseq.class/Card]
+            [?b :block/uuid]]
+          db)
+     (apply concat))))
+
+(defn get-due-card-block-ids
+  [repo]
+  (let [db (db/get-db repo)
+        now-inst-ms (inst-ms (js/Date.))]
+    (->> (d/q '[:find ?b
+                :in $ ?now-inst-ms
+                :where
+                [?b :block/tags :logseq.class/Card]
+                [?b :logseq.property.fsrs/due ?due-b]
+                [?due-b :property.value/content ?due]
+                [(>= ?now-inst-ms ?due)]
+                [?b :block/uuid]]
+              db now-inst-ms)
+         (apply concat))))
+
+(defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
+  (ui/button
+   [:span btn-text (when-not (util/sm-breakpoint?)
+                     [" " (ui/render-keyboard-shortcut shortcut {:theme :text})])]
+   :id id
+   :class (str id " " class)
+   :background background
+   :on-pointer-down (fn [e] (util/stop-propagation e))
+   :on-click (fn [_e]
+               (js/setTimeout #(on-click) 10))))
+
+(def ^:private phase->next-phase
+  {:init :show-answer
+   :show-answer :end
+   :end :end})
+
+(rum/defcs card <
+  (rum/local :init ::phase)
+  [state repo block-entity]
+  (let [*phase (::phase state)]
+    [:div.ls-card.content
+     [:div (component-block/breadcrumb {} repo (:block/uuid block-entity) {})]
+     (component-block/blocks-container
+      (cond-> {}
+        (contains? #{:init} @*phase) (assoc :hide-children? true))
+      [block-entity])
+     (btn-with-shortcut {:btn-text (t :flashcards/modal-btn-show-answers)
+                         :shortcut "s"
+                         :id (str "card-answers")
+                         :on-click #(swap! *phase phase->next-phase)})]))
+
+;; {
+;;    :again 1 ;; We got the answer wrong. Automatically means that we
+;;             ;; have forgotten the card. This is a lapse in memory.
+;;    :hard  2 ;; The answer was only partially correct and/or we took
+;;             ;; too long to recall it.
+;;    :good  3 ;; The answer was correct but we were not confident about it.
+;;    :easy  4 ;; The answer was correct and we were confident and quick
+;;             ;; in our recall.
+;;    }
+(def ^:private rating->shortcut
+  {:again "1"
+   :hard  "2"
+   :good  "3"
+   :easy  "4"})
+
+(defn- rating-btns
+  [repo block-id *card-index]
+  (mapv
+   (fn [rating]
+     (btn-with-shortcut {:btn-text (name rating)
+                         :shortcut (rating->shortcut rating)
+                         :id (str "card-" (name rating))
+                         :on-click #(do (repeat-card! repo block-id rating)
+                                        (swap! *card-index inc))}))
+   (keys rating->shortcut)))
+
+(rum/defcs cards <
+  (rum/local 0 ::card-index)
+  [state]
+  (let [repo (state/get-current-repo)
+        block-ids (get-card-block-ids repo)
+        *card-index (::card-index state)]
+    (if-let [block-entity (some-> (nth block-ids @*card-index nil) db/entity)]
+      (vec (concat [:div (card repo block-entity)]
+                   (rating-btns repo (:db/id block-entity) *card-index)))
+      [:p.p-2 (t :flashcards/modal-finished)])))

+ 7 - 5
src/main/frontend/handler/events.cljs

@@ -79,7 +79,8 @@
             [frontend.date :as date]
             [logseq.db :as ldb]
             [frontend.persist-db :as persist-db]
-            [frontend.handler.export :as export]))
+            [frontend.handler.export :as export]
+            [frontend.extensions.fsrs]))
 
 ;; TODO: should we move all events here?
 
@@ -289,10 +290,11 @@
       (shui/dialog-open! {:align :top}))))
 
 (defmethod handle :modal/show-cards [_]
-  (shui/dialog-open!
-    srs/global-cards
-    {:id :srs
-     :label "flashcards__cp"}))
+  (let [db-based? (config/db-based-graph? (state/get-current-repo))]
+    (shui/dialog-open!
+     (if db-based? frontend.extensions.fsrs/cards srs/global-cards)
+     {:id :srs
+      :label "flashcards__cp"})))
 
 (defmethod handle :modal/show-instruction [_]
   (shui/dialog-open!

+ 2 - 2
src/main/frontend/worker/rtc/db_listener.cljs

@@ -26,8 +26,8 @@
     :db/index :db/valueType :db/cardinality})
 
 (def ^:private watched-attr-ns
-  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.task"
-    "logseq.property.linked-references"
+  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs"
+    "logseq.property.linked-references" "logseq.task"
     "logseq.class" "logseq.kv"})
 
 (defn- watched-attr?