srs.cljs 28 KB

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