srs.cljs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811
  1. (ns frontend.extensions.srs
  2. (:require [frontend.template :as template]
  3. [frontend.db.query-dsl :as query-dsl]
  4. [frontend.db.query-react :as query-react]
  5. [frontend.util :as util]
  6. [logseq.graph-parser.property :as gp-property]
  7. [logseq.graph-parser.util.page-ref :as page-ref]
  8. [frontend.util.property :as property]
  9. [frontend.util.drawer :as drawer]
  10. [frontend.util.persist-var :as persist-var]
  11. [frontend.db :as db]
  12. [frontend.db.model :as db-model]
  13. [frontend.db-mixins :as db-mixins]
  14. [frontend.state :as state]
  15. [frontend.handler.editor :as editor-handler]
  16. [frontend.handler.editor.property :as editor-property]
  17. [frontend.components.block :as component-block]
  18. [frontend.components.macro :as component-macro]
  19. [frontend.components.select :as component-select]
  20. [frontend.components.svg :as svg]
  21. [frontend.ui :as ui]
  22. [frontend.date :as date]
  23. [frontend.commands :as commands]
  24. [frontend.components.editor :as editor]
  25. [cljs-time.core :as t]
  26. [cljs-time.local :as tl]
  27. [cljs-time.coerce :as tc]
  28. [clojure.string :as string]
  29. [rum.core :as rum]
  30. [frontend.modules.shortcut.core :as shortcut]
  31. [medley.core :as medley]))
  32. ;;; ================================================================
  33. ;;; Commentary
  34. ;;; - One block with tag "#card" or "[[card]]" is treated as a card.
  35. ;;; - {{cloze content}} show as "[...]" when reviewing cards
  36. ;;; ================================================================
  37. ;;; const & vars
  38. ;; TODO: simplify state
  39. (defonce global-cards-mode? (atom false))
  40. (def card-hash-tag "card")
  41. (def card-last-interval-property :card-last-interval)
  42. (def card-repeats-property :card-repeats)
  43. (def card-last-reviewed-property :card-last-reviewed)
  44. (def card-next-schedule-property :card-next-schedule)
  45. (def card-last-easiness-factor-property :card-ease-factor)
  46. (def card-last-score-property :card-last-score)
  47. (def default-card-properties-map {card-last-interval-property -1
  48. card-repeats-property 0
  49. card-last-easiness-factor-property 2.5})
  50. (def cloze-macro-name
  51. "cloze syntax: {{cloze: ...}}"
  52. "cloze")
  53. (def query-macro-name
  54. "{{cards ...}}"
  55. "cards")
  56. (def learning-fraction-default
  57. "any number between 0 and 1 (the greater it is the faster the changes of the OF matrix)"
  58. 0.5)
  59. (defn- learning-fraction []
  60. (if-let [learning-fraction (:srs/learning-fraction (state/get-config))]
  61. (if (and (number? learning-fraction)
  62. (< learning-fraction 1)
  63. (> learning-fraction 0))
  64. learning-fraction
  65. learning-fraction-default)
  66. learning-fraction-default))
  67. (def of-matrix (persist-var/persist-var nil "srs-of-matrix"))
  68. (def initial-interval-default 4)
  69. (defn- initial-interval []
  70. (if-let [initial-interval (:srs/initial-interval (state/get-config))]
  71. (if (and (number? initial-interval)
  72. (> initial-interval 0))
  73. initial-interval
  74. initial-interval-default)
  75. initial-interval-default))
  76. ;;; ================================================================
  77. ;;; utils
  78. (defn- get-block-card-properties
  79. [block]
  80. (when-let [properties (:block/properties block)]
  81. (merge
  82. default-card-properties-map
  83. (select-keys properties [card-last-interval-property
  84. card-repeats-property
  85. card-last-reviewed-property
  86. card-next-schedule-property
  87. card-last-easiness-factor-property
  88. card-last-score-property]))))
  89. (defn- save-block-card-properties!
  90. [block props]
  91. (editor-handler/save-block-if-changed!
  92. block
  93. (property/insert-properties (:block/format block) (:block/content block) props)
  94. {:force? true}))
  95. (defn- reset-block-card-properties!
  96. [block]
  97. (save-block-card-properties! block {card-last-interval-property -1
  98. card-repeats-property 0
  99. card-last-easiness-factor-property 2.5
  100. card-last-reviewed-property "nil"
  101. card-next-schedule-property "nil"
  102. card-last-score-property "nil"}))
  103. ;;; used by other ns
  104. (defn card-block?
  105. [block]
  106. (let [card-entity (db/entity [:block/name card-hash-tag])
  107. refs (into #{} (:block/refs block))]
  108. (contains? refs card-entity)))
  109. (declare get-root-block)
  110. ;;; ================================================================
  111. ;;; sr algorithm (sm-5)
  112. ;;; https://www.supermemo.com/zh/archives1990-2015/english/ol/sm5
  113. (defn- fix-2f
  114. [n]
  115. (/ (Math/round (* 100 n)) 100))
  116. (defn- get-of [of-matrix n ef]
  117. (or (get-in of-matrix [n ef])
  118. (if (<= n 1)
  119. (initial-interval)
  120. ef)))
  121. (defn- set-of [of-matrix n ef of]
  122. (->>
  123. (fix-2f of)
  124. (assoc-in of-matrix [n ef])))
  125. (defn- interval
  126. [n ef of-matrix]
  127. (if (<= n 1)
  128. (get-of of-matrix 1 ef)
  129. (* (get-of of-matrix n ef)
  130. (interval (- n 1) ef of-matrix))))
  131. (defn- next-ef
  132. [ef quality]
  133. (let [ef* (+ ef (- 0.1 (* (- 5 quality) (+ 0.08 (* 0.02 (- 5 quality))))))]
  134. (if (< ef* 1.3) 1.3 ef*)))
  135. (defn- next-of-matrix
  136. [of-matrix n quality fraction ef]
  137. (let [of (get-of of-matrix n ef)
  138. of* (* of (+ 0.72 (* quality 0.07)))
  139. of** (+ (* (- 1 fraction) of) (* of* fraction))]
  140. (set-of of-matrix n ef of**)))
  141. (defn next-interval
  142. "return [next-interval repeats next-ef of-matrix]"
  143. [_last-interval repeats ef quality of-matrix]
  144. (assert (and (<= quality 5) (>= quality 0)))
  145. (let [ef (or ef 2.5)
  146. next-ef (next-ef ef quality)
  147. next-of-matrix (next-of-matrix of-matrix repeats quality (learning-fraction) ef)
  148. next-interval (interval repeats next-ef next-of-matrix)]
  149. (if (< quality 3)
  150. ;; If the quality response was lower than 3
  151. ;; then start repetitions for the item from
  152. ;; the beginning without changing the E-Factor
  153. [-1 1 ef next-of-matrix]
  154. [(fix-2f next-interval) (+ 1 repeats) (fix-2f next-ef) next-of-matrix])))
  155. ;;; ================================================================
  156. ;;; card protocol
  157. (defprotocol ICard
  158. (get-root-block [this]))
  159. (defprotocol ICardShow
  160. ;; return {:value blocks :next-phase next-phase}
  161. (show-cycle [this phase])
  162. (show-cycle-config [this phase]))
  163. (defn- has-cloze?
  164. [blocks]
  165. (->> (map :block/content blocks)
  166. (some #(string/includes? % "{{cloze "))))
  167. (defn- clear-collapsed-property
  168. "Clear block's collapsed property if exists"
  169. [blocks]
  170. (let [result (map (fn [block]
  171. (-> block
  172. (dissoc :block/collapsed?)
  173. (medley/dissoc-in [:block/properties :collapsed]))) blocks)]
  174. result))
  175. ;;; ================================================================
  176. ;;; card impl
  177. (deftype Sided-Cloze-Card [block]
  178. ICard
  179. (get-root-block [_this] (db/pull [:block/uuid block]))
  180. ICardShow
  181. (show-cycle [_this phase]
  182. (let [block-id (:db/id block)
  183. blocks (-> (db/get-paginated-blocks (state/get-current-repo) block-id
  184. {:scoped-block-id block-id})
  185. clear-collapsed-property)
  186. cloze? (has-cloze? blocks)]
  187. (case phase
  188. 1
  189. (let [blocks-count (count blocks)]
  190. {:value [(first blocks)] :next-phase (if (or (> blocks-count 1) (nil? cloze?)) 2 3)})
  191. 2
  192. {:value blocks :next-phase (if cloze? 3 1)}
  193. 3
  194. {:value blocks :next-phase 1})))
  195. (show-cycle-config [_this phase]
  196. (case phase
  197. 1
  198. {}
  199. 2
  200. {}
  201. 3
  202. {:show-cloze? true})))
  203. (defn- ->card [block]
  204. (let [block' (db/pull (:db/id block))]
  205. (->Sided-Cloze-Card block')))
  206. ;;; ================================================================
  207. ;;;
  208. (defn- query
  209. "Use same syntax as frontend.db.query-dsl.
  210. Add an extra condition: block's :block/refs contains `#card or [[card]]'"
  211. ([repo query-string]
  212. (query repo query-string {}))
  213. ([repo query-string {:keys [use-cache?]
  214. :or {use-cache? true}}]
  215. (when (string? query-string)
  216. (let [result (if (string/blank? query-string)
  217. (:block/_refs (db/entity [:block/name card-hash-tag]))
  218. (let [query-string (template/resolve-dynamic-template! query-string)
  219. query-string (if-not (or (string/blank? query-string)
  220. (string/starts-with? query-string "(")
  221. (string/starts-with? query-string "["))
  222. (page-ref/->page-ref (string/trim query-string))
  223. query-string)
  224. {:keys [query sort-by rules]} (query-dsl/parse query-string)
  225. query* (util/concat-without-nil
  226. [['?b :block/refs '?br] ['?br :block/name card-hash-tag]]
  227. (if (coll? (first query)) query [query]))]
  228. (when-let [query (query-dsl/query-wrapper query*
  229. {:blocks? true
  230. :block-attrs [:db/id :block/properties]})]
  231. (let [result (query-react/react-query repo
  232. {:query (with-meta query {:cards-query? true})
  233. :rules (or rules [])}
  234. (merge
  235. {:use-cache? use-cache?}
  236. (when sort-by
  237. {:transform-fn sort-by})))]
  238. (when result
  239. (flatten (util/react result)))))))]
  240. (vec result)))))
  241. (defn- query-scheduled
  242. "Return blocks scheduled to 'time' or before"
  243. [blocks time]
  244. (let [filtered-result (filterv (fn [b]
  245. (let [props (:block/properties b)
  246. next-sched (get props card-next-schedule-property)
  247. next-sched* (tc/from-string next-sched)
  248. repeats (get props card-repeats-property)]
  249. (or (nil? repeats)
  250. (< repeats 1)
  251. (nil? next-sched)
  252. (nil? next-sched*)
  253. (t/before? next-sched* time))))
  254. blocks),
  255. sort-by-next-schedule (sort-by (fn [b]
  256. (get (get b :block/properties) card-next-schedule-property)) filtered-result)]
  257. {:total (count blocks)
  258. :result sort-by-next-schedule}))
  259. ;;; ================================================================
  260. ;;; operations
  261. (defn- get-next-interval
  262. [card score]
  263. {:pre [(and (<= score 5) (>= score 0))
  264. (satisfies? ICard card)]}
  265. (let [block (.-block card)
  266. props (get-block-card-properties block)
  267. last-interval (or
  268. (when-let [v (get props card-last-interval-property)]
  269. (util/safe-parse-float v))
  270. 0)
  271. repeats (or (when-let [v (get props card-repeats-property)]
  272. (util/safe-parse-int v))
  273. 0)
  274. last-ef (or (when-let [v (get props card-last-easiness-factor-property)]
  275. (util/safe-parse-float v)) 2.5)
  276. [next-interval next-repeats next-ef of-matrix*]
  277. (next-interval last-interval repeats last-ef score @of-matrix)
  278. next-interval* (if (< next-interval 0) 0 next-interval)
  279. next-schedule (tc/to-string (t/plus (tl/local-now) (t/hours (* 24 next-interval*))))
  280. now (tc/to-string (tl/local-now))]
  281. {:next-of-matrix of-matrix*
  282. card-last-interval-property next-interval
  283. card-repeats-property next-repeats
  284. card-last-easiness-factor-property next-ef
  285. card-next-schedule-property next-schedule
  286. card-last-reviewed-property now
  287. card-last-score-property score}))
  288. (defn- operation-score!
  289. [card score]
  290. {:pre [(and (<= score 5) (>= score 0))
  291. (satisfies? ICard card)]}
  292. (let [block (.-block card)
  293. result (get-next-interval card score)
  294. next-of-matrix (:next-of-matrix result)]
  295. (reset! of-matrix next-of-matrix)
  296. (save-block-card-properties! (db/pull (:db/id block))
  297. (select-keys result
  298. [card-last-interval-property
  299. card-repeats-property
  300. card-last-easiness-factor-property
  301. card-next-schedule-property
  302. card-last-reviewed-property
  303. card-last-score-property]))))
  304. (defn- operation-reset!
  305. [card]
  306. {:pre [(satisfies? ICard card)]}
  307. (let [block (.-block card)]
  308. (reset-block-card-properties! (db/pull (:db/id block)))))
  309. (defn- operation-card-info-summary!
  310. [review-records review-cards card-query-block]
  311. (when card-query-block
  312. (let [review-count (count (flatten (vals review-records)))
  313. review-cards-count (count review-cards)
  314. score-remembered-count (+ (count (get review-records 5))
  315. (count (get review-records 3)))
  316. score-forgotten-count (count (get review-records 1))]
  317. (editor-handler/insert-block-tree-after-target
  318. (:db/id card-query-block) false
  319. [{:content (util/format "Summary: %d items, %d review counts [[%s]]"
  320. review-cards-count review-count (date/today))
  321. :children [{:content
  322. (util/format "Remembered: %d (%d%%)" score-remembered-count (* 100 (/ score-remembered-count review-count)))}
  323. {:content
  324. (util/format "Forgotten : %d (%d%%)" score-forgotten-count (* 100 (/ score-forgotten-count review-count)))}]}]
  325. (:block/format card-query-block)
  326. false))))
  327. ;;; ================================================================
  328. ;;; UI
  329. (defn- dec-cards-due-count!
  330. []
  331. (state/update-state! :srs/cards-due-count
  332. (fn [n]
  333. (if (> n 0)
  334. (dec n)
  335. n))))
  336. (defn- score-and-next-card [score card *card-index finished? *phase *review-records cb]
  337. (operation-score! card score)
  338. (swap! *review-records #(update % score (fn [ov] (conj ov card))))
  339. (if finished?
  340. (when cb (cb @*review-records))
  341. (reset! *phase 1))
  342. (swap! *card-index inc)
  343. (when @global-cards-mode?
  344. (dec-cards-due-count!)))
  345. (defn- skip-card [card *card-index finished? *phase *review-records cb]
  346. (swap! *review-records #(update % "skip" (fn [ov] (conj ov card))))
  347. (swap! *card-index inc)
  348. (if finished?
  349. (when cb (cb @*review-records))
  350. (reset! *phase 1)))
  351. (def review-finished
  352. [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
  353. (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
  354. (ui/button
  355. [:span btn-text (when-not (util/sm-breakpoint?)
  356. [" " (ui/render-keyboard-shortcut shortcut)])]
  357. :id id
  358. :class (str id " " class)
  359. :background background
  360. :on-mouse-down (fn [e] (util/stop-propagation e))
  361. :on-click (fn [_e]
  362. (js/setTimeout #(on-click) 10))))
  363. (rum/defcs view < rum/reactive db-mixins/query
  364. (rum/local 1 ::phase)
  365. (rum/local {} ::review-records)
  366. [state blocks {preview? :preview?
  367. cards? :cards?
  368. modal? :modal?
  369. cb :callback}
  370. card-index]
  371. (let [review-records (::review-records state)
  372. current-block (util/nth-safe blocks @card-index)
  373. card (when current-block (->card current-block))
  374. finished? (= (inc @card-index) (count blocks))]
  375. (if (nil? card)
  376. review-finished
  377. (let [phase (::phase state)
  378. {current-blocks :value next-phase :next-phase} (show-cycle card @phase)
  379. root-block (.-block card)
  380. root-block-id (:block/uuid root-block)]
  381. [:div.ls-card.content
  382. {:class (when (or preview? modal?)
  383. (str (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto")
  384. (when modal? " modal-cards")))}
  385. (let [repo (state/get-current-repo)]
  386. [:div {:style {:margin-top 20}}
  387. (component-block/breadcrumb {} repo root-block-id {})])
  388. (component-block/blocks-container
  389. current-blocks
  390. (merge (show-cycle-config card @phase)
  391. {:id (str root-block-id)
  392. :editor-box editor/box
  393. :review-cards? true}))
  394. (if (or preview? modal?)
  395. [:div.flex.my-4.justify-between
  396. (when-not (and (not preview?) (= next-phase 1))
  397. (btn-with-shortcut {:btn-text (case next-phase
  398. 1 "Hide answers"
  399. 2 "Show answers"
  400. 3 "Show clozes")
  401. :shortcut "s"
  402. :id "card-answers"
  403. :class "mr-2"
  404. :on-click #(reset! phase next-phase)}))
  405. (when (and (not= @card-index (count blocks))
  406. cards?
  407. preview?)
  408. (btn-with-shortcut {:btn-text "Next"
  409. :shortcut "n"
  410. :id "card-next"
  411. :class "mr-2"
  412. :on-click (fn [e]
  413. (util/stop e)
  414. (skip-card card card-index finished? phase review-records cb))}))
  415. (when (and (not preview?) (= 1 next-phase))
  416. [:<>
  417. (btn-with-shortcut {:btn-text "Forgotten"
  418. :shortcut "f"
  419. :id "card-forgotten"
  420. :background "red"
  421. :on-click (fn []
  422. (score-and-next-card 1 card card-index finished? phase review-records cb)
  423. (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
  424. (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
  425. (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
  426. :shortcut "t"
  427. :id "card-recall"
  428. :on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
  429. (btn-with-shortcut {:btn-text "Remembered"
  430. :shortcut "r"
  431. :id "card-remembered"
  432. :background "green"
  433. :on-click #(score-and-next-card 5 card card-index finished? phase review-records cb)})])
  434. (when preview?
  435. (ui/tippy {:html [:div.text-sm
  436. "Reset this card so that you can review it immediately."]
  437. :class "tippy-hover"
  438. :interactive true}
  439. (ui/button [:span "Reset"]
  440. :id "card-reset"
  441. :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
  442. :on-click (fn [e]
  443. (util/stop e)
  444. (operation-reset! card)))))]
  445. [:div.my-3 (ui/button "Review cards" :small? true)])]))))
  446. (rum/defc view-modal <
  447. (shortcut/mixin :shortcut.handler/cards)
  448. [blocks option card-index]
  449. [:div#cards-modal
  450. (if (seq blocks)
  451. (rum/with-key
  452. (view blocks option card-index)
  453. (str "ls-card-" (:db/id (first blocks))))
  454. review-finished)])
  455. (rum/defc preview-cp < rum/reactive db-mixins/query
  456. [block-id]
  457. (let [blocks (db/get-paginated-blocks (state/get-current-repo) block-id
  458. {:scoped-block-id block-id})]
  459. (view-modal blocks {:preview? true} (atom 0))))
  460. (defn preview
  461. [block-id]
  462. (state/set-modal! #(preview-cp block-id) {:id :srs}))
  463. ;;; ================================================================
  464. ;;; register some external vars & related UI
  465. ;;; register cloze macro
  466. (def ^:private cloze-cue-separator "\\\\")
  467. (defn- cloze-parse
  468. "Parse the cloze content, and return [answer cue]."
  469. [content]
  470. (let [parts (string/split content cloze-cue-separator -1)]
  471. (if (<= (count parts) 1)
  472. [content nil]
  473. (let [cue (string/trim (last parts))]
  474. ;; If there are more than one separator, only the last component is considered the cue.
  475. [(string/trimr (string/join cloze-cue-separator (drop-last parts))) cue]))))
  476. (rum/defcs cloze-macro-show < rum/reactive
  477. {:init (fn [state]
  478. (let [config (first (:rum/args state))
  479. shown? (atom (:show-cloze? config))]
  480. (assoc state :shown? shown?)))}
  481. [state config options]
  482. (let [shown?* (:shown? state)
  483. shown? (rum/react shown?*)
  484. toggle! #(swap! shown?* not)
  485. [answer cue] (cloze-parse (string/join ", " (:arguments options)))]
  486. (if (or shown? (:show-cloze? config))
  487. [:a.cloze-revealed {:on-click toggle!}
  488. (util/format "[%s]" answer)]
  489. [:a.cloze {:on-click toggle!}
  490. (if (string/blank? cue)
  491. "[...]"
  492. (str "(" cue ")"))])))
  493. (component-macro/register cloze-macro-name cloze-macro-show)
  494. (def cards-total (atom 0))
  495. (defn get-srs-cards-total
  496. []
  497. (try
  498. (let [repo (state/get-current-repo)
  499. query-string ""
  500. blocks (query repo query-string {:use-cache? false})]
  501. (when (seq blocks)
  502. (let [{:keys [result]} (query-scheduled blocks (tl/local-now))
  503. count (count result)]
  504. (reset! cards-total count)
  505. count)))
  506. (catch :default e
  507. (js/console.error e) 0)))
  508. (declare cards)
  509. (rum/defc cards-select
  510. [{:keys [on-chosen]}]
  511. (let [cards (db-model/get-macro-blocks (state/get-current-repo) "cards")
  512. items (->> (map (comp :logseq.macro-arguments :block/properties) cards)
  513. (map (fn [col] (string/join " " col))))
  514. items (concat items ["All"])]
  515. (component-select/select {:items items
  516. :on-chosen on-chosen
  517. :close-modal? false
  518. :input-default-placeholder "Switch to"
  519. :extract-fn nil})))
  520. ;;; register cards macro
  521. (rum/defcs ^:large-vars/cleanup-todo cards-inner < rum/reactive db-mixins/query
  522. (rum/local 0 ::card-index)
  523. (rum/local false ::random-mode?)
  524. (rum/local false ::preview-mode?)
  525. [state config options {:keys [query-atom query-string query-result due-result]}]
  526. (let [*random-mode? (::random-mode? state)
  527. *preview-mode? (::preview-mode? state)
  528. *card-index (::card-index state)]
  529. (if (seq query-result)
  530. (let [{:keys [total result]} due-result
  531. review-cards (if @*preview-mode? query-result result)
  532. card-query-block (db/entity [:block/uuid (:block/uuid config)])
  533. filtered-total (count result)
  534. modal? (:modal? config)
  535. callback-fn (fn [review-records]
  536. (when-not @*preview-mode?
  537. (operation-card-info-summary!
  538. review-records review-cards card-query-block)
  539. (persist-var/persist-save of-matrix)))]
  540. [:div.flex-1.cards-review {:style (when modal? {:height "100%"})}
  541. [:div.flex.flex-row.items-center.justify-between.cards-title
  542. [:div.flex.flex-row.items-center
  543. (ui/icon "infinity" {:style {:font-size 20}})
  544. (ui/dropdown
  545. (fn [{:keys [toggle-fn]}]
  546. [:div.ml-1.text-sm.font-medium.cursor
  547. {:on-mouse-down (fn [e]
  548. (util/stop e)
  549. (toggle-fn))}
  550. [:span.flex (if (string/blank? query-string) "All" query-string)
  551. [:span {:style {:margin-top 2}}
  552. (svg/caret-down)]]])
  553. (fn [{:keys [toggle-fn]}]
  554. (cards-select {:on-chosen (fn [query]
  555. (let [query' (if (= query "All") "" query)]
  556. (reset! query-atom query')
  557. (toggle-fn)))}))
  558. {:modal-class (util/hiccup->class
  559. "origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg")})]
  560. [:div.flex.flex-row.items-center
  561. ;; FIXME: CSS issue
  562. (if @*preview-mode?
  563. (ui/tippy {:html [:div.text-sm "current/total"]
  564. :interactive true}
  565. [:div.opacity-60.text-sm.mr-3
  566. @*card-index
  567. [:span "/"]
  568. total])
  569. (ui/tippy {:html [:div.text-sm "overdue/total"]
  570. ;; :class "tippy-hover"
  571. :interactive true}
  572. [:div.opacity-60.text-sm.mr-3
  573. (max 0 (- filtered-total @*card-index))
  574. [:span "/"]
  575. total]))
  576. (ui/tippy
  577. {:html [:div.text-sm "Toggle preview mode"]
  578. :delay [1000, 100]
  579. :class "tippy-hover"
  580. :interactive true
  581. :disabled false}
  582. [:a.opacity-60.hover:opacity-100.svg-small.inline.font-bold
  583. {:id "preview-all-cards"
  584. :style (when @*preview-mode? {:color "orange"})
  585. :on-click (fn [e]
  586. (util/stop e)
  587. (swap! *preview-mode? not)
  588. (reset! *card-index 0))}
  589. "A"])
  590. (ui/tippy
  591. {:html [:div.text-sm "Toggle random mode"]
  592. :delay [1000, 100]
  593. :class "tippy-hover"
  594. :interactive true}
  595. [:a.mt-1.ml-2.block.opacity-60.hover:opacity-100
  596. {:on-mouse-down (fn [e]
  597. (util/stop e)
  598. (swap! *random-mode? not))}
  599. (ui/icon "arrows-shuffle" {:style (cond->
  600. {:font-size 18
  601. :font-weight 600}
  602. @*random-mode?
  603. (assoc :color "orange"))})])]]
  604. [:div.px-1
  605. (when (and (not modal?) (not @*preview-mode?))
  606. {:on-click (fn []
  607. (state/set-modal! #(cards (assoc config :modal? true) {:query-string query-string})
  608. {:id :srs}))})
  609. (let [view-fn (if modal? view-modal view)
  610. blocks (if @*preview-mode? query-result review-cards)
  611. blocks (if @*random-mode? (shuffle blocks) blocks)]
  612. (view-fn blocks
  613. (merge config
  614. (merge options
  615. {:random-mode? @*random-mode?
  616. :preview? @*preview-mode?
  617. :callback callback-fn}))
  618. *card-index))]])
  619. (if (:global? config)
  620. [:div.ls-card.content
  621. [:h1.title "Time to create a card!"]
  622. [:div
  623. [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]
  624. [:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
  625. [:p "You can "
  626. [:a {:href "https://docs.logseq.com/#/page/cards" :target "_blank"}
  627. "click this link"]
  628. " to check the documentation."]]]
  629. [:div.opacity-60.custom-query-title.ls-card.content
  630. [:div.w-full.flex-1
  631. [:code.p-1 (str "Cards: " query-string)]]
  632. [:div.mt-2.ml-2.font-medium "No matched cards"]]))))
  633. (rum/defcs cards <
  634. (rum/local nil ::query)
  635. {:will-mount (fn [state]
  636. (state/set-state! :srs/mode? true)
  637. state)
  638. :will-unmount (fn [state]
  639. (state/set-state! :srs/mode? false)
  640. state)}
  641. [state config options]
  642. (let [*query (::query state)
  643. repo (state/get-current-repo)
  644. query-string (or @*query
  645. (:query-string options)
  646. (string/join ", " (:arguments options)))
  647. query-result (query repo query-string)
  648. due-result (query-scheduled query-result (tl/local-now))]
  649. (cards-inner config (assoc options :cards? true)
  650. {:query-atom *query
  651. :query-string query-string
  652. :query-result query-result
  653. :due-result due-result})))
  654. (rum/defc global-cards <
  655. {:will-mount (fn [state]
  656. (reset! global-cards-mode? true)
  657. state)
  658. :will-unmount (fn [state]
  659. (reset! global-cards-mode? false)
  660. state)}
  661. []
  662. (cards {:modal? true
  663. :global? true} {}))
  664. (component-macro/register query-macro-name cards)
  665. ;;; register builtin properties
  666. (gp-property/register-built-in-properties #{card-last-interval-property
  667. card-repeats-property
  668. card-last-reviewed-property
  669. card-next-schedule-property
  670. card-last-easiness-factor-property
  671. card-last-score-property})
  672. ;;; register slash commands
  673. (commands/register-slash-command ["Cards"
  674. [[:editor/input "{{cards }}" {:backward-pos 2}]]
  675. "Create a cards query"])
  676. (commands/register-slash-command ["Cloze"
  677. [[:editor/input "{{cloze }}" {:backward-pos 2}]]
  678. "Create a cloze"])
  679. ;; handlers
  680. (defn make-block-a-card!
  681. [block-id]
  682. (when-let [block (db/entity [:block/uuid block-id])]
  683. (when-let [content (:block/content block)]
  684. (let [content (-> (property/remove-built-in-properties (:block/format block) content)
  685. (drawer/remove-logbook))]
  686. (editor-handler/save-block!
  687. (state/get-current-repo)
  688. block-id
  689. (str (string/trim content) " #" card-hash-tag))))))
  690. (defn batch-make-cards!
  691. ([] (batch-make-cards! (state/get-selection-block-ids)))
  692. ([block-ids]
  693. (let [block-content-fn (fn [block]
  694. [block (-> (property/remove-built-in-properties (:block/format block) (:block/content block))
  695. (drawer/remove-logbook)
  696. string/trim
  697. (str " #" card-hash-tag))])
  698. blocks (->> block-ids
  699. (map #(db/entity [:block/uuid %]))
  700. (remove card-block?)
  701. (map #(db/pull [:block/uuid (:block/uuid %)]))
  702. (map block-content-fn))]
  703. (when-not (empty? blocks)
  704. (editor-handler/save-blocks! blocks)))))
  705. (defonce *due-cards-interval (atom nil))
  706. (defn update-cards-due-count!
  707. []
  708. (when (state/enable-flashcards?)
  709. (let [f (fn []
  710. (let [total (get-srs-cards-total)]
  711. (state/set-state! :srs/cards-due-count total)))]
  712. (js/setTimeout f 1000)
  713. (when (nil? @*due-cards-interval)
  714. ;; refresh every hour
  715. (let [interval (js/setInterval f (* 3600 1000))]
  716. (reset! *due-cards-interval interval))))))