mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
remove old srs implementation
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.extensions.fsrs :as fsrs]
|
||||
[frontend.extensions.srs :as srs]
|
||||
[frontend.handler.common.developer :as dev-common-handler]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
@@ -100,9 +99,7 @@
|
||||
(when (state/enable-flashcards?)
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Make a Card"
|
||||
:on-click #(if (config/db-based-graph? (state/get-current-repo))
|
||||
(fsrs/batch-make-cards!)
|
||||
(srs/batch-make-cards!))}
|
||||
:on-click (fsrs/batch-make-cards!)}
|
||||
(t :context-menu/make-a-flashcard)))
|
||||
|
||||
(shui/dropdown-menu-item
|
||||
@@ -285,17 +282,10 @@
|
||||
(block-template block-id))
|
||||
|
||||
(cond
|
||||
(srs/card-block? block)
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Preview Card"
|
||||
:on-click #(srs/preview (:db/id block))}
|
||||
(t :context-menu/preview-flashcard))
|
||||
(state/enable-flashcards?)
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Make a Card"
|
||||
:on-click #(if (config/db-based-graph? (state/get-current-repo))
|
||||
(fsrs/batch-make-cards! [block-id])
|
||||
(srs/batch-make-cards! [block-id]))}
|
||||
:on-click #(fsrs/batch-make-cards! [block-id])}
|
||||
(t :context-menu/make-a-flashcard))
|
||||
:else
|
||||
nil)
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.common.missionary :as c.m]
|
||||
[frontend.components.block :as component-block]
|
||||
[frontend.config :as config]
|
||||
[frontend.components.macro :as component-macro]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.async :as db-async]
|
||||
[frontend.db.model :as db-model]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.extensions.srs :as srs]
|
||||
[frontend.handler.block :as block-handler]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.modules.shortcut.core :as shortcut]
|
||||
@@ -305,14 +304,12 @@
|
||||
"Return a task that update `:srs/cards-due-count` periodically."
|
||||
(m/sp
|
||||
(let [repo (state/get-current-repo)]
|
||||
(if (config/db-based-graph? repo)
|
||||
(m/?
|
||||
(m/reduce
|
||||
(fn [_ _]
|
||||
(p/let [due-cards (<get-due-card-block-ids repo nil)]
|
||||
(state/set-state! :srs/cards-due-count (count due-cards))))
|
||||
(c.m/clock (* 3600 1000))))
|
||||
(srs/update-cards-due-count!)))))
|
||||
(m/?
|
||||
(m/reduce
|
||||
(fn [_ _]
|
||||
(p/let [due-cards (<get-due-card-block-ids repo nil)]
|
||||
(state/set-state! :srs/cards-due-count (count due-cards))))
|
||||
(c.m/clock (* 3600 1000)))))))
|
||||
|
||||
(defn update-due-cards-count
|
||||
[]
|
||||
@@ -344,6 +341,49 @@
|
||||
:block/tags
|
||||
(:db/id (db/entity :logseq.class/Card)))))))
|
||||
|
||||
;;; register cloze macro
|
||||
|
||||
(def ^:private cloze-cue-separator "\\\\")
|
||||
|
||||
(defn- cloze-parse
|
||||
"Parse the cloze content, and return [answer cue]."
|
||||
[content]
|
||||
(let [parts (string/split content cloze-cue-separator -1)]
|
||||
(if (<= (count parts) 1)
|
||||
[content nil]
|
||||
(let [cue (string/trim (last parts))]
|
||||
;; If there are more than one separator, only the last component is considered the cue.
|
||||
[(string/trimr (string/join cloze-cue-separator (drop-last parts))) cue]))))
|
||||
|
||||
(rum/defcs cloze-macro-show < rum/reactive
|
||||
{:init (fn [state]
|
||||
(let [config (first (:rum/args state))
|
||||
shown? (atom (:show-cloze? config))]
|
||||
(assoc state :shown? shown?)))}
|
||||
[state config options]
|
||||
(let [shown?* (:shown? state)
|
||||
shown? (rum/react shown?*)
|
||||
toggle! #(swap! shown?* not)
|
||||
[answer cue] (cloze-parse (string/join ", " (:arguments options)))]
|
||||
(if (or shown? (:show-cloze? config))
|
||||
[:a.cloze-revealed {:on-click toggle!}
|
||||
(util/format "[%s]" answer)]
|
||||
[:a.cloze {:on-click toggle!}
|
||||
(if (string/blank? cue)
|
||||
"[...]"
|
||||
(str "(" cue ")"))])))
|
||||
|
||||
(def cloze-macro-name
|
||||
"cloze syntax: {{cloze: ...}}"
|
||||
"cloze")
|
||||
|
||||
;; TODO: support {{cards ...}}
|
||||
;; (def query-macro-name
|
||||
;; "{{cards ...}}"
|
||||
;; "cards")
|
||||
|
||||
(component-macro/register cloze-macro-name cloze-macro-show)
|
||||
|
||||
(comment
|
||||
(defn- cards-in-time-range
|
||||
[cards start-instant end-instant]
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
(ns frontend.extensions.srs
|
||||
"SRS fns for file based graphs. Will be deprecated in db-based version.
|
||||
See also `frontend.extensions.fsrs`"
|
||||
(:require [cljs-time.coerce :as tc]
|
||||
[cljs-time.core :as t]
|
||||
[cljs-time.local :as tl]
|
||||
[clojure.string :as string]
|
||||
[frontend.commands :as commands]
|
||||
[frontend.components.block :as component-block]
|
||||
[frontend.components.editor :as editor]
|
||||
[frontend.components.macro :as component-macro]
|
||||
[frontend.components.select :as component-select]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.date :as date]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.db.query-react :as query-react]
|
||||
[frontend.format.mldoc :as mldoc]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.property.file :as property-file]
|
||||
[frontend.modules.shortcut.core :as shortcut]
|
||||
[frontend.state :as state]
|
||||
[frontend.template :as template]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.util :as util]
|
||||
[frontend.util.file-based.drawer :as drawer]
|
||||
[frontend.util.persist-var :as persist-var]
|
||||
[logseq.graph-parser.property :as gp-property]
|
||||
[logseq.shui.ui :as shui]
|
||||
[medley.core :as medley]
|
||||
[rum.core :as rum]))
|
||||
|
||||
;;; ================================================================
|
||||
;;; Commentary
|
||||
;;; - One block with tag "#card" or "[[card]]" is treated as a card.
|
||||
;;; - {{cloze content}} show as "[...]" when reviewing cards
|
||||
|
||||
;;; ================================================================
|
||||
;;; const & vars
|
||||
|
||||
;; TODO: simplify state
|
||||
|
||||
(defonce global-cards-mode? (atom false))
|
||||
|
||||
(def card-hash-tag "card")
|
||||
|
||||
(def card-last-interval-property :card-last-interval)
|
||||
(def card-repeats-property :card-repeats)
|
||||
(def card-last-reviewed-property :card-last-reviewed)
|
||||
(def card-next-schedule-property :card-next-schedule)
|
||||
(def card-last-easiness-factor-property :card-ease-factor)
|
||||
(def card-last-score-property :card-last-score)
|
||||
|
||||
(def default-card-properties-map {card-last-interval-property -1
|
||||
card-repeats-property 0
|
||||
card-last-easiness-factor-property 2.5})
|
||||
|
||||
(def cloze-macro-name
|
||||
"cloze syntax: {{cloze: ...}}"
|
||||
"cloze")
|
||||
|
||||
(def query-macro-name
|
||||
"{{cards ...}}"
|
||||
"cards")
|
||||
|
||||
(def learning-fraction-default
|
||||
"any number between 0 and 1 (the greater it is the faster the changes of the OF matrix)"
|
||||
0.5)
|
||||
|
||||
(defn- get-learning-fraction []
|
||||
(if-let [learning-fraction (:srs/learning-fraction (state/get-config))]
|
||||
(if (and (number? learning-fraction)
|
||||
(< learning-fraction 1)
|
||||
(> learning-fraction 0))
|
||||
learning-fraction
|
||||
learning-fraction-default)
|
||||
learning-fraction-default))
|
||||
|
||||
(def srs-of-matrix (persist-var/persist-var nil "srs-of-matrix"))
|
||||
|
||||
(def initial-interval-default 4)
|
||||
|
||||
(defn- get-initial-interval []
|
||||
(if-let [initial-interval (:srs/initial-interval (state/get-config))]
|
||||
(if (and (number? initial-interval)
|
||||
(> initial-interval 0))
|
||||
initial-interval
|
||||
initial-interval-default)
|
||||
initial-interval-default))
|
||||
|
||||
;;; ================================================================
|
||||
;;; utils
|
||||
|
||||
;; FIXME: All uses of properties in this namespace for db
|
||||
(defn- get-block-card-properties
|
||||
[block]
|
||||
(when-let [properties (:block/properties block)]
|
||||
(merge
|
||||
default-card-properties-map
|
||||
(select-keys properties [card-last-interval-property
|
||||
card-repeats-property
|
||||
card-last-reviewed-property
|
||||
card-next-schedule-property
|
||||
card-last-easiness-factor-property
|
||||
card-last-score-property]))))
|
||||
|
||||
(defn- save-block-card-properties!
|
||||
[block props]
|
||||
(editor-handler/save-block-if-changed!
|
||||
block
|
||||
(property-file/insert-properties-when-file-based
|
||||
(state/get-current-repo) (:block/format block) (:block/title block) props)
|
||||
{:force? true}))
|
||||
|
||||
(defn- reset-block-card-properties!
|
||||
[block]
|
||||
(save-block-card-properties! block {card-last-interval-property -1
|
||||
card-repeats-property 0
|
||||
card-last-easiness-factor-property 2.5
|
||||
card-last-reviewed-property "nil"
|
||||
card-next-schedule-property "nil"
|
||||
card-last-score-property "nil"}))
|
||||
|
||||
;;; used by other ns
|
||||
|
||||
(defn card-block?
|
||||
[block]
|
||||
(let [card-entity (db/get-page card-hash-tag)
|
||||
refs (into #{} (:block/refs block))]
|
||||
(contains? refs card-entity)))
|
||||
|
||||
(declare get-root-block)
|
||||
|
||||
;;; ================================================================
|
||||
;;; sr algorithm (sm-5)
|
||||
;;; https://www.supermemo.com/zh/archives1990-2015/english/ol/sm5
|
||||
|
||||
(defn- fix-2f
|
||||
[n]
|
||||
(/ (Math/round (* 100 n)) 100))
|
||||
|
||||
(defn- get-of [of-matrix n ef]
|
||||
(or (get-in of-matrix [n ef])
|
||||
(if (<= n 1)
|
||||
(get-initial-interval)
|
||||
ef)))
|
||||
|
||||
(defn- set-of [of-matrix n ef of]
|
||||
(->>
|
||||
(fix-2f of)
|
||||
(assoc-in of-matrix [n ef])))
|
||||
|
||||
(defn- interval
|
||||
[n ef of-matrix]
|
||||
(if (<= n 1)
|
||||
(get-of of-matrix 1 ef)
|
||||
(* (get-of of-matrix n ef)
|
||||
(interval (- n 1) ef of-matrix))))
|
||||
|
||||
(defn- get-next-ef
|
||||
[ef quality]
|
||||
(let [ef* (+ ef (- 0.1 (* (- 5 quality) (+ 0.08 (* 0.02 (- 5 quality))))))]
|
||||
(if (< ef* 1.3) 1.3 ef*)))
|
||||
|
||||
(defn- get-next-of-matrix
|
||||
[of-matrix n quality fraction ef]
|
||||
(let [of (get-of of-matrix n ef)
|
||||
of* (* of (+ 0.72 (* quality 0.07)))
|
||||
of** (+ (* (- 1 fraction) of) (* of* fraction))]
|
||||
(set-of of-matrix n ef of**)))
|
||||
|
||||
(defn calc-next-interval
|
||||
"return [next-interval repeats next-ef of-matrix]"
|
||||
[_last-interval repeats ef quality of-matrix]
|
||||
(assert (and (<= quality 5) (>= quality 0)))
|
||||
(let [ef (or ef 2.5)
|
||||
next-ef (get-next-ef ef quality)
|
||||
next-of-matrix (get-next-of-matrix of-matrix repeats quality (get-learning-fraction) ef)
|
||||
next-interval (interval repeats next-ef next-of-matrix)]
|
||||
|
||||
(if (< quality 3)
|
||||
;; If the quality response was lower than 3
|
||||
;; then start repetitions for the item from
|
||||
;; the beginning without changing the E-Factor
|
||||
[-1 1 ef next-of-matrix]
|
||||
[(fix-2f next-interval) (+ 1 repeats) (fix-2f next-ef) next-of-matrix])))
|
||||
|
||||
;;; ================================================================
|
||||
;;; card protocol
|
||||
|
||||
(defprotocol ICard
|
||||
(get-root-block [this]))
|
||||
|
||||
(defprotocol ICardShow
|
||||
;; return {:value blocks :next-phase next-phase}
|
||||
(show-cycle [this phase])
|
||||
|
||||
(show-cycle-config [this phase]))
|
||||
|
||||
(defn- has-cloze?
|
||||
[blocks]
|
||||
(->> (map :block/title blocks)
|
||||
(some #(string/includes? % "{{cloze "))))
|
||||
|
||||
(defn- clear-collapsed-property
|
||||
"Clear block's collapsed property if exists"
|
||||
[blocks]
|
||||
(let [result (map (fn [block]
|
||||
(-> block
|
||||
(dissoc :block/collapsed?)
|
||||
(medley/dissoc-in [:block/properties :collapsed]))) blocks)]
|
||||
result))
|
||||
|
||||
;;; ================================================================
|
||||
;;; card impl
|
||||
|
||||
(deftype Sided-Cloze-Card [block]
|
||||
ICard
|
||||
(get-root-block [_this] (db/pull [:block/uuid block]))
|
||||
ICardShow
|
||||
(show-cycle [_this phase]
|
||||
(let [block-id (:db/id block)
|
||||
blocks (-> [(db/entity block-id)]
|
||||
clear-collapsed-property)
|
||||
cloze? (has-cloze? blocks)]
|
||||
(case phase
|
||||
1
|
||||
(let [blocks-count (count blocks)]
|
||||
{:value [(first blocks)] :next-phase (if (or (> blocks-count 1) cloze?) 2 3)})
|
||||
2
|
||||
{:value blocks :next-phase (if cloze? 3 1)}
|
||||
3
|
||||
{:value blocks :next-phase 1})))
|
||||
|
||||
(show-cycle-config [_this phase]
|
||||
(case phase
|
||||
1
|
||||
{:hide-children? true}
|
||||
2
|
||||
{}
|
||||
3
|
||||
{:show-cloze? true})))
|
||||
|
||||
(defn- ->card [block]
|
||||
(let [block' (db/pull (:db/id block))]
|
||||
(->Sided-Cloze-Card block')))
|
||||
|
||||
;;; ================================================================
|
||||
;;;
|
||||
|
||||
(defn- query
|
||||
"Use same syntax as frontend.db.query-dsl.
|
||||
Add an extra condition: block's :block/refs contains `#card or [[card]]'"
|
||||
([repo query-string]
|
||||
(query repo query-string {}))
|
||||
([repo query-string {:keys [use-cache?]
|
||||
:or {use-cache? true}}]
|
||||
(when (string? query-string)
|
||||
(let [result (if (string/blank? query-string)
|
||||
(:block/_refs (db/get-page card-hash-tag))
|
||||
(let [query-string (template/resolve-dynamic-template! query-string)
|
||||
{query* :query :keys [sort-by rules]} (query-dsl/parse query-string {:db-graph? (config/db-based-graph? repo)})]
|
||||
(when-let [query' (query-dsl/query-wrapper query*
|
||||
{:blocks? true
|
||||
:block-attrs [:db/id :block/properties]})]
|
||||
(let [result (last (query-react/react-query repo
|
||||
{:query (with-meta query' {:cards-query? true})
|
||||
:rules (or rules [])}
|
||||
(merge
|
||||
{:use-cache? use-cache?}
|
||||
(when sort-by
|
||||
{:transform-fn sort-by}))))]
|
||||
(when result
|
||||
(flatten (util/react result)))))))]
|
||||
(vec result)))))
|
||||
|
||||
(defn- query-scheduled
|
||||
"Return blocks scheduled to 'time' or before"
|
||||
[blocks time]
|
||||
(let [filtered-result (filterv (fn [b]
|
||||
(let [props (:block/properties b)
|
||||
next-sched (get props card-next-schedule-property)
|
||||
next-sched* (tc/from-string next-sched)
|
||||
repeats (get props card-repeats-property)]
|
||||
(or (nil? repeats)
|
||||
(< repeats 1)
|
||||
(nil? next-sched)
|
||||
(nil? next-sched*)
|
||||
(t/before? next-sched* time))))
|
||||
blocks),
|
||||
sort-by-next-schedule (sort-by (fn [b]
|
||||
(get (get b :block/properties) card-next-schedule-property)) filtered-result)]
|
||||
{:total (count blocks)
|
||||
:result sort-by-next-schedule}))
|
||||
|
||||
;;; ================================================================
|
||||
;;; operations
|
||||
|
||||
(defn- get-next-interval
|
||||
[card score]
|
||||
{:pre [(and (<= score 5) (>= score 0))
|
||||
(satisfies? ICard card)]}
|
||||
(let [block (.-block card)
|
||||
props (get-block-card-properties block)
|
||||
last-interval (or
|
||||
(when-let [v (get props card-last-interval-property)]
|
||||
(util/safe-parse-float v))
|
||||
0)
|
||||
repeats (or (when-let [v (get props card-repeats-property)]
|
||||
(util/safe-parse-int v))
|
||||
0)
|
||||
last-ef (or (when-let [v (get props card-last-easiness-factor-property)]
|
||||
(util/safe-parse-float v)) 2.5)
|
||||
[next-interval next-repeats next-ef of-matrix*]
|
||||
(calc-next-interval last-interval repeats last-ef score @srs-of-matrix)
|
||||
next-interval* (if (< next-interval 0) 0 next-interval)
|
||||
next-schedule (tc/to-string (t/plus (tl/local-now) (t/hours (* 24 next-interval*))))
|
||||
now (tc/to-string (tl/local-now))]
|
||||
{:next-of-matrix of-matrix*
|
||||
card-last-interval-property next-interval
|
||||
card-repeats-property next-repeats
|
||||
card-last-easiness-factor-property next-ef
|
||||
card-next-schedule-property next-schedule
|
||||
card-last-reviewed-property now
|
||||
card-last-score-property score}))
|
||||
|
||||
(defn- operation-score!
|
||||
[card score]
|
||||
{:pre [(and (<= score 5) (>= score 0))
|
||||
(satisfies? ICard card)]}
|
||||
(let [block (.-block card)
|
||||
result (get-next-interval card score)
|
||||
next-of-matrix (:next-of-matrix result)]
|
||||
(reset! srs-of-matrix next-of-matrix)
|
||||
(save-block-card-properties! (db/pull (:db/id block))
|
||||
(select-keys result
|
||||
[card-last-interval-property
|
||||
card-repeats-property
|
||||
card-last-easiness-factor-property
|
||||
card-next-schedule-property
|
||||
card-last-reviewed-property
|
||||
card-last-score-property]))))
|
||||
|
||||
(defn- operation-reset!
|
||||
[card]
|
||||
{:pre [(satisfies? ICard card)]}
|
||||
(let [block (.-block card)]
|
||||
(reset-block-card-properties! (db/pull (:db/id block)))))
|
||||
|
||||
(defn- operation-card-info-summary!
|
||||
[review-records review-cards card-query-block]
|
||||
(when card-query-block
|
||||
(let [review-count (count (flatten (vals review-records)))
|
||||
review-cards-count (count review-cards)
|
||||
score-remembered-count (+ (count (get review-records 5))
|
||||
(count (get review-records 3)))
|
||||
score-forgotten-count (count (get review-records 1))]
|
||||
(editor-handler/insert-block-tree-after-target
|
||||
(:db/id card-query-block) false
|
||||
[{:content (util/format "Summary: %d items, %d review counts [[%s]]"
|
||||
review-cards-count review-count (date/today))
|
||||
:children [{:content
|
||||
(util/format "Remembered: %d (%d%%)" score-remembered-count (* 100 (/ score-remembered-count review-count)))}
|
||||
{:content
|
||||
(util/format "Forgotten : %d (%d%%)" score-forgotten-count (* 100 (/ score-forgotten-count review-count)))}]}]
|
||||
(:block/format card-query-block)
|
||||
false))))
|
||||
|
||||
;;; ================================================================
|
||||
;;; UI
|
||||
|
||||
(defn- dec-cards-due-count!
|
||||
[]
|
||||
(state/update-state! :srs/cards-due-count
|
||||
(fn [n]
|
||||
(if (> n 0)
|
||||
(dec n)
|
||||
n))))
|
||||
|
||||
(defn- score-and-next-card [score card *card-index finished? *phase *review-records cb]
|
||||
(operation-score! card score)
|
||||
(swap! *review-records #(update % score (fn [ov] (conj ov card))))
|
||||
(if finished?
|
||||
(when cb (cb @*review-records))
|
||||
(reset! *phase 1))
|
||||
(swap! *card-index inc)
|
||||
(when @global-cards-mode?
|
||||
(dec-cards-due-count!)))
|
||||
|
||||
(defn- skip-card [card *card-index finished? *phase *review-records cb]
|
||||
(swap! *review-records #(update % "skip" (fn [ov] (conj ov card))))
|
||||
(swap! *card-index inc)
|
||||
(if finished?
|
||||
(when cb (cb @*review-records))
|
||||
(reset! *phase 1)))
|
||||
|
||||
(def review-finished
|
||||
[:p.p-2 (t :flashcards/modal-finished)])
|
||||
|
||||
(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))))
|
||||
|
||||
(rum/defcs view < rum/reactive db-mixins/query
|
||||
(rum/local 1 ::phase)
|
||||
(rum/local {} ::review-records)
|
||||
[state blocks {preview? :preview?
|
||||
cards? :cards?
|
||||
modal? :modal?
|
||||
cb :callback}
|
||||
card-index]
|
||||
(let [review-records (::review-records state)
|
||||
current-block (util/nth-safe blocks @card-index)
|
||||
card (when current-block (->card current-block))
|
||||
finished? (= (inc @card-index) (count blocks))]
|
||||
(if (nil? card)
|
||||
review-finished
|
||||
(let [phase (::phase state)
|
||||
{current-blocks :value next-phase :next-phase} (show-cycle card @phase)
|
||||
root-block (.-block card)
|
||||
root-block-id (:block/uuid root-block)]
|
||||
[:div.ls-card.content
|
||||
{:class (when (or preview? modal?)
|
||||
(str (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto")
|
||||
(when modal? " modal-cards")))}
|
||||
(let [repo (state/get-current-repo)]
|
||||
[:div {:style {:margin-top 20}}
|
||||
(component-block/breadcrumb {} repo root-block-id {})])
|
||||
(component-block/blocks-container
|
||||
(merge (show-cycle-config card @phase)
|
||||
{:id (str root-block-id)
|
||||
:editor-box editor/box
|
||||
:review-cards? true})
|
||||
current-blocks)
|
||||
(if (or preview? modal?)
|
||||
[:div.flex.my-4.justify-between
|
||||
(when-not (and (not preview?) (= next-phase 1))
|
||||
(btn-with-shortcut {:btn-text (case next-phase
|
||||
1 (t :flashcards/modal-btn-hide-answers)
|
||||
2 (t :flashcards/modal-btn-show-answers)
|
||||
3 (t :flashcards/modal-btn-show-clozes))
|
||||
:shortcut "s"
|
||||
:id "card-answers"
|
||||
:class "mr-2"
|
||||
:on-click #(reset! phase next-phase)}))
|
||||
(when (and (not= @card-index (count blocks))
|
||||
cards?
|
||||
preview?)
|
||||
(btn-with-shortcut {:btn-text (t :flashcards/modal-btn-next-card)
|
||||
:shortcut "n"
|
||||
:id "card-next"
|
||||
:class "mr-2"
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(skip-card card card-index finished? phase review-records cb))}))
|
||||
|
||||
(when (and (not preview?) (= 1 next-phase))
|
||||
[:<>
|
||||
(btn-with-shortcut {:btn-text (t :flashcards/modal-btn-forgotten)
|
||||
:shortcut "f"
|
||||
:id "card-forgotten"
|
||||
:background "red"
|
||||
:on-click (fn []
|
||||
(score-and-next-card 1 card card-index finished? phase review-records cb)
|
||||
(let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
|
||||
(property-handler/set-block-property! (state/get-current-repo) root-block-id card-next-schedule-property tomorrow)))})
|
||||
|
||||
(btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" (t :flashcards/modal-btn-recall))
|
||||
:shortcut "t"
|
||||
:id "card-recall"
|
||||
:on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
|
||||
|
||||
(btn-with-shortcut {:btn-text (t :flashcards/modal-btn-remembered)
|
||||
:shortcut "r"
|
||||
:id "card-remembered"
|
||||
:background "green"
|
||||
:on-click #(score-and-next-card 5 card card-index finished? phase review-records cb)})])
|
||||
|
||||
(when preview?
|
||||
(ui/tooltip
|
||||
(ui/button [:span (t :flashcards/modal-btn-reset)]
|
||||
:id "card-reset"
|
||||
:class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(operation-reset! card)))
|
||||
|
||||
[:div.text-sm
|
||||
(t :flashcards/modal-btn-reset-tip)]
|
||||
{:trigger-props {:as-child false}}))]
|
||||
[:div.my-3 (ui/button "Review cards" :small? true)])]))))
|
||||
|
||||
(rum/defc view-modal <
|
||||
(shortcut/mixin :shortcut.handler/cards false)
|
||||
[blocks option card-index]
|
||||
[:div#cards-modal
|
||||
(if (seq blocks)
|
||||
(rum/with-key
|
||||
(view blocks option card-index)
|
||||
(str "ls-card-" (:db/id (first blocks))))
|
||||
review-finished)])
|
||||
|
||||
(rum/defc preview-cp < rum/reactive db-mixins/query
|
||||
[block-id]
|
||||
(let [blocks [(db/entity block-id)]]
|
||||
(view-modal blocks {:preview? true} (atom 0))))
|
||||
|
||||
(defn preview
|
||||
[block-id]
|
||||
(shui/dialog-open! #(preview-cp block-id) {:id :srs}))
|
||||
|
||||
;;; ================================================================
|
||||
;;; register some external vars & related UI
|
||||
|
||||
;;; register cloze macro
|
||||
|
||||
(def ^:private cloze-cue-separator "\\\\")
|
||||
|
||||
(defn- cloze-parse
|
||||
"Parse the cloze content, and return [answer cue]."
|
||||
[content]
|
||||
(let [parts (string/split content cloze-cue-separator -1)]
|
||||
(if (<= (count parts) 1)
|
||||
[content nil]
|
||||
(let [cue (string/trim (last parts))]
|
||||
;; If there are more than one separator, only the last component is considered the cue.
|
||||
[(string/trimr (string/join cloze-cue-separator (drop-last parts))) cue]))))
|
||||
|
||||
(rum/defcs cloze-macro-show < rum/reactive
|
||||
{:init (fn [state]
|
||||
(let [config (first (:rum/args state))
|
||||
shown? (atom (:show-cloze? config))]
|
||||
(assoc state :shown? shown?)))}
|
||||
[state config options]
|
||||
(let [shown?* (:shown? state)
|
||||
shown? (rum/react shown?*)
|
||||
toggle! #(swap! shown?* not)
|
||||
[answer cue] (cloze-parse (string/join ", " (:arguments options)))]
|
||||
(if (or shown? (:show-cloze? config))
|
||||
[:a.cloze-revealed {:on-click toggle!}
|
||||
(util/format "[%s]" answer)]
|
||||
[:a.cloze {:on-click toggle!}
|
||||
(if (string/blank? cue)
|
||||
"[...]"
|
||||
(str "(" cue ")"))])))
|
||||
|
||||
(component-macro/register cloze-macro-name cloze-macro-show)
|
||||
|
||||
(def cards-total (atom 0))
|
||||
|
||||
(defn get-srs-cards-total
|
||||
[]
|
||||
(try
|
||||
(let [repo (state/get-current-repo)
|
||||
query-string ""
|
||||
blocks (query repo query-string {:use-cache? false})]
|
||||
(when (seq blocks)
|
||||
(let [{:keys [result]} (query-scheduled blocks (tl/local-now))
|
||||
count (count result)]
|
||||
(reset! cards-total count)
|
||||
count)))
|
||||
(catch :default e
|
||||
(js/console.error e) 0)))
|
||||
|
||||
(declare cards)
|
||||
|
||||
;; TODO: FIXME: macros have been deleted
|
||||
(rum/defc cards-select
|
||||
[{:keys [on-chosen]}]
|
||||
(let [items [(t :flashcards/modal-select-all)]]
|
||||
(component-select/select {:items items
|
||||
:on-chosen on-chosen
|
||||
:close-modal? false
|
||||
:input-default-placeholder (t :flashcards/modal-select-switch)
|
||||
:extract-fn nil})))
|
||||
|
||||
;;; register cards macro
|
||||
(rum/defcs ^:large-vars/cleanup-todo cards-inner < rum/reactive db-mixins/query
|
||||
(rum/local 0 ::card-index)
|
||||
(rum/local false ::random-mode?)
|
||||
(rum/local false ::preview-mode?)
|
||||
[state config options {:keys [query-atom query-string query-result due-result]}]
|
||||
(let [*random-mode? (::random-mode? state)
|
||||
*preview-mode? (::preview-mode? state)
|
||||
*card-index (::card-index state)]
|
||||
(if (seq query-result)
|
||||
(let [{:keys [total result]} due-result
|
||||
review-cards (if @*preview-mode? query-result result)
|
||||
card-query-block (db/entity [:block/uuid (:block/uuid config)])
|
||||
filtered-total (count result)
|
||||
modal? (:modal? config)
|
||||
callback-fn (fn [review-records]
|
||||
(when-not @*preview-mode?
|
||||
(operation-card-info-summary!
|
||||
review-records review-cards card-query-block)
|
||||
(persist-var/persist-save srs-of-matrix)))]
|
||||
[:div.flex-1.cards-review {:style (when modal? {:height "100%"})}
|
||||
[:div.flex.flex-row.items-center.justify-between.cards-title
|
||||
[:div.flex.flex-row.items-center
|
||||
(ui/icon "infinity" {:style {:font-size 20}})
|
||||
(ui/dropdown
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
[:div.ml-1.text-sm.font-medium.cursor
|
||||
{:on-pointer-down (fn [e]
|
||||
(util/stop e)
|
||||
(toggle-fn))}
|
||||
[:span.flex (if (string/blank? query-string) (t :flashcards/modal-select-all) query-string)
|
||||
[:span {:style {:margin-top 2}}
|
||||
(svg/caret-down)]]])
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
(cards-select {:on-chosen (fn [query']
|
||||
(let [query'' (if (= query' (t :flashcards/modal-select-all)) "" query')]
|
||||
(reset! query-atom query'')
|
||||
(toggle-fn)))}))
|
||||
{:modal-class (util/hiccup->class
|
||||
"origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg")})]
|
||||
|
||||
[:div.flex.flex-row.items-center
|
||||
|
||||
;; FIXME: CSS issue
|
||||
(if @*preview-mode?
|
||||
(ui/tooltip
|
||||
[:div.opacity-60.text-sm.mr-2
|
||||
@*card-index
|
||||
[:span "/"]
|
||||
total]
|
||||
[:div.text-sm (t :flashcards/modal-current-total)])
|
||||
(ui/tooltip
|
||||
[:div.opacity-60.text-sm.mr-2
|
||||
(max 0 (- filtered-total @*card-index))
|
||||
[:span "/"]
|
||||
total]
|
||||
[:div.text-sm (t :flashcards/modal-overdue-total)]))
|
||||
|
||||
(ui/tooltip
|
||||
(ui/button
|
||||
(merge
|
||||
{:icon "letter-a"
|
||||
:intent "link"
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(swap! *preview-mode? not)
|
||||
(reset! *card-index 0))
|
||||
:button-props {:id "preview-all-cards"}
|
||||
:small? true}
|
||||
(when @*preview-mode?
|
||||
{:icon-props {:style {:color "var(--ls-button-background)"}}})))
|
||||
[:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
|
||||
{:trigger-props {:as-child false}})
|
||||
|
||||
(ui/tooltip
|
||||
(ui/button
|
||||
(merge
|
||||
{:icon "arrows-shuffle"
|
||||
:intent "link"
|
||||
:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(swap! *random-mode? not))
|
||||
:small? true}
|
||||
(when @*random-mode?
|
||||
{:icon-props {:style {:color "var(--ls-button-background)"}}})))
|
||||
[:div.text-sm (t :flashcards/modal-toggle-random-mode)]
|
||||
{:trigger-props {:as-child false}})]]
|
||||
[:div.px-1
|
||||
(when (and (not modal?) (not @*preview-mode?))
|
||||
{:on-click (fn []
|
||||
(shui/dialog-open!
|
||||
#(cards (assoc config :modal? true) {:query-string query-string})
|
||||
{:id :srs}))})
|
||||
(let [view-fn (if modal? view-modal view)
|
||||
blocks (if @*preview-mode? query-result review-cards)
|
||||
blocks (if @*random-mode? (shuffle blocks) blocks)]
|
||||
(view-fn blocks
|
||||
(merge config
|
||||
(merge options
|
||||
{:random-mode? @*random-mode?
|
||||
:preview? @*preview-mode?
|
||||
:callback callback-fn}))
|
||||
*card-index))]])
|
||||
(if (:global? config)
|
||||
[:div.ls-card.content
|
||||
[:h1.title (t :flashcards/modal-welcome-title)]
|
||||
|
||||
[:div
|
||||
[:p (t :flashcards/modal-welcome-desc-1 "#card")]
|
||||
[:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
|
||||
[:p (t :flashcards/modal-welcome-desc-2)
|
||||
[:a {:href "https://docs.logseq.com/#/page/Flashcards" :target "_blank"}
|
||||
(t :flashcards/modal-welcome-desc-3)]
|
||||
(t :flashcards/modal-welcome-desc-4)]]]
|
||||
[:div.opacity-60.custom-query-title.ls-card.content
|
||||
[:div.w-full.flex-1
|
||||
[:code.p-1 (str "Cards: " query-string)]]
|
||||
[:div.mt-2.ml-2.font-medium "No matched cards"]]))))
|
||||
|
||||
(rum/defcs cards <
|
||||
(rum/local nil ::query)
|
||||
{:will-mount (fn [state]
|
||||
(state/set-state! :srs/mode? true)
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(state/set-state! :srs/mode? false)
|
||||
state)}
|
||||
[state config options]
|
||||
(let [*query (::query state)
|
||||
repo (state/get-current-repo)
|
||||
query-string (or @*query
|
||||
(:query-string options)
|
||||
(string/join ", " (:arguments options)))
|
||||
query-result (query repo query-string)
|
||||
due-result (query-scheduled query-result (tl/local-now))]
|
||||
(cards-inner config (assoc options :cards? true)
|
||||
{:query-atom *query
|
||||
:query-string query-string
|
||||
:query-result query-result
|
||||
:due-result due-result})))
|
||||
|
||||
(rum/defc global-cards <
|
||||
{:will-mount (fn [state]
|
||||
(reset! global-cards-mode? true)
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(reset! global-cards-mode? false)
|
||||
state)}
|
||||
[]
|
||||
(cards {:modal? true
|
||||
:global? true} {}))
|
||||
|
||||
(component-macro/register query-macro-name cards)
|
||||
|
||||
;;; register builtin properties
|
||||
(gp-property/register-built-in-properties #{card-last-interval-property
|
||||
card-repeats-property
|
||||
card-last-reviewed-property
|
||||
card-next-schedule-property
|
||||
card-last-easiness-factor-property
|
||||
card-last-score-property})
|
||||
|
||||
;;; register slash commands
|
||||
(commands/register-slash-command ["Cards"
|
||||
[[:editor/input "{{cards }}" {:backward-pos 2}]]
|
||||
"Create a cards query"
|
||||
{:icon :icon/cards
|
||||
:db-graph? false}])
|
||||
|
||||
(commands/register-slash-command ["Cloze"
|
||||
[[:editor/input "{{cloze }}" {:backward-pos 2}]]
|
||||
"Create a cloze"
|
||||
{:icon :icon/eye-question}])
|
||||
|
||||
;; handlers
|
||||
(defn add-card-tag-to-block
|
||||
"given a block struct, adds the #card to title and returns
|
||||
a seq of [original-block new-content-string]"
|
||||
[block]
|
||||
(when-let [content (:block/title block)]
|
||||
(let [format (get block :block/format :markdown)
|
||||
content (-> (property-file/remove-built-in-properties-when-file-based
|
||||
(state/get-current-repo) format content)
|
||||
(drawer/remove-logbook))
|
||||
[title body] (mldoc/get-title&body content format)]
|
||||
[block (str title " #" card-hash-tag "\n" body)])))
|
||||
|
||||
(defn batch-make-cards!
|
||||
([] (batch-make-cards! (state/get-selection-block-ids)))
|
||||
([block-ids]
|
||||
(let [valid-blocks (->> block-ids
|
||||
(map #(db/entity [:block/uuid %]))
|
||||
(remove card-block?)
|
||||
(map #(db/pull [:block/uuid (:block/uuid %)])))
|
||||
blocks (map add-card-tag-to-block valid-blocks)]
|
||||
(when-not (empty? blocks)
|
||||
(editor-handler/save-blocks! blocks)))))
|
||||
|
||||
(defonce *due-cards-interval (atom nil))
|
||||
|
||||
(defn update-cards-due-count!
|
||||
[]
|
||||
(when (state/enable-flashcards?)
|
||||
(let [f (fn []
|
||||
(let [total (get-srs-cards-total)]
|
||||
(state/set-state! :srs/cards-due-count total)))]
|
||||
(js/setTimeout f 1000)
|
||||
(when (nil? @*due-cards-interval)
|
||||
;; refresh every hour
|
||||
(let [interval' (js/setInterval f (* 3600 1000))]
|
||||
(reset! *due-cards-interval interval'))))))
|
||||
@@ -1,24 +0,0 @@
|
||||
/******************************************************************************/
|
||||
/** Review widget *************************************************************/
|
||||
/******************************************************************************/
|
||||
.cards-review {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cards-title {
|
||||
border-radius: 4px;
|
||||
background: var(--color-level-1);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.cp__right-sidebar .cards-title,
|
||||
|
||||
/******************************************************************************/
|
||||
/** Card blocks ***************************************************************/
|
||||
/******************************************************************************/
|
||||
div[data-refs-self*='"card"'] {
|
||||
margin-bottom: 8px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
[frontend.config :as config]
|
||||
[frontend.db :as db]
|
||||
[frontend.extensions.fsrs :as fsrs]
|
||||
[frontend.extensions.srs :as srs]
|
||||
[frontend.handler.db-based.rtc :as rtc-handler]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.events :as events]
|
||||
@@ -91,11 +90,10 @@
|
||||
(component-page/batch-delete-dialog selected-rows ok-handler)))
|
||||
|
||||
(defmethod events/handle :modal/show-cards [[_ cards-id]]
|
||||
(let [db-based? (config/db-based-graph? (state/get-current-repo))]
|
||||
(shui/dialog-open!
|
||||
(if db-based? (fn [] (fsrs/cards-view cards-id)) srs/global-cards)
|
||||
{:id :srs
|
||||
:label "flashcards__cp"})))
|
||||
(shui/dialog-open!
|
||||
(fn [] (fsrs/cards-view cards-id))
|
||||
{:id :srs
|
||||
:label "flashcards__cp"}))
|
||||
|
||||
(defmethod events/handle :modal/show-themes-modal [[_ classic?]]
|
||||
(if classic?
|
||||
|
||||
Reference in New Issue
Block a user