Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions app/(tabs)/lists/study.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1651,14 +1651,17 @@ function StudyScreen() {
// Store in pools — refillDeque will pull from these
duePoolRef.current = dueItems;
newPoolRef.current = newItems;
dueIdsRef.current = new Set(dueItems.map((item) => item.srsCard!.id));
// A flagged-card review is built fresh as all-new cards, so its
// progress counts every card rather than only the due ones.
const countedItems = isEphemeralListId(listId) ? [...dueItems, ...newItems] : dueItems;
dueIdsRef.current = new Set(countedItems.map((item) => item.srsCard!.id));

// Seed the deque: refill from empty state pulls first ~10
const seeded = refillDeque([], 0);
setCards(seeded);
setCursor(0);
setOriginalCardCount(seeded.length);
setDueAtStart(dueRows.length);
setDueAtStart(countedItems.length);
completedSrsIdsRef.current.clear();
setSessionPhase("studying");
} else {
Expand Down Expand Up @@ -1908,7 +1911,7 @@ function StudyScreen() {
shouldReQueue = true;
reQueueItem = { ...item, srsCard: rated.card };
} else if (dueIdsRef.current.has(card.id)) {
// Only count due cards toward progress (not new cards)
// Only cards counted at session start advance progress
completedSrsIdsRef.current.add(card.id);
}
} else if (card) {
Expand Down Expand Up @@ -2295,9 +2298,13 @@ function StudyScreen() {
if (list?.confusionDetection === false) return;
if (sessionMode === "add_order") return;

// Check cooldown: skip if we checked this card recently
if (card.lastConfusionCheck) {
const lastCheck = new Date(card.lastConfusionCheck).getTime();
// Check cooldown against the DB: a re-queued copy's row predates this session's checks.
const checked = await userDb.getFirstAsync<{ last_confusion_check: string | null }>(
"SELECT last_confusion_check FROM srs_cards WHERE id = ?",
[card.id],
);
if (checked?.last_confusion_check) {
const lastCheck = new Date(checked.last_confusion_check).getTime();
const cooldownMs = CONFUSION_COOLDOWN_HOURS * 60 * 60 * 1000;
if (Date.now() - lastCheck < cooldownMs) return;
}
Expand Down Expand Up @@ -2886,7 +2893,8 @@ function StudyScreen() {
>
{isBrowsingHistory
? (() => {
const frontier = cards.findIndex((c) => c.status === "pending");
// A flipped-but-unanswered card is the frontier too, not only a pending one.
const frontier = cards.findIndex((c) => c.status !== "rated");
const stepsBack = (frontier === -1 ? cards.length : frontier) - cursor;
return `\u2190 ${stepsBack} back`;
})()
Expand Down
52 changes: 35 additions & 17 deletions components/SmartReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { useUserDb } from "@/db/user-provider";
import { dayResetHourAtom, smartReviewDaysAtom } from "@/stores/settings";
import { countMarkedInWindow, getOrCreateSmartList } from "@/lib/smart-review";
import {
countMarkedInWindow,
getOrCreateSmartList,
type SmartReviewWindow,
} from "@/lib/smart-review";
import { useRouter } from "expo-router";
import type { WordList } from "@/db/types";

const DAY_OPTIONS = [3, 7, 14, 30] as const;
const WINDOW_OPTIONS: SmartReviewWindow[] = [3, 7, 14, 30, null];

function windowLabel(days: SmartReviewWindow): string {
return days == null ? "All" : `${days}d`;
}

function windowPhrase(days: SmartReviewWindow): string {
return days == null ? "in total" : `in the last ${days} days`;
}

interface SmartReviewModalProps {
visible: boolean;
Expand All @@ -22,24 +34,27 @@ export function SmartReviewModal({ visible, onClose, sourceList }: SmartReviewMo
const userDb = useUserDb();
const [days, setDays] = useAtom(smartReviewDaysAtom);
const [resetHour] = useAtom(dayResetHourAtom);
const [count, setCount] = useState<number | null>(null);
const [counts, setCounts] = useState<Map<SmartReviewWindow, number> | null>(null);
const [starting, setStarting] = useState(false);

useEffect(() => {
if (!visible || !userDb || !sourceList) return;
let cancelled = false;
setCount(null);
countMarkedInWindow(userDb, sourceList.id, days, resetHour)
.then((n) => {
if (!cancelled) setCount(n);
setCounts(null);
Promise.all(WINDOW_OPTIONS.map((d) => countMarkedInWindow(userDb, sourceList.id, d, resetHour)))
.then((ns) => {
if (!cancelled) setCounts(new Map(WINDOW_OPTIONS.map((d, i) => [d, ns[i]])));
})
.catch(() => {
if (!cancelled) setCount(0);
.catch((err) => {
console.error("[SmartReview] count failed:", err);
if (!cancelled) setCounts(new Map());
});
return () => {
cancelled = true;
};
}, [visible, userDb, sourceList?.id, days, resetHour]);
}, [visible, userDb, sourceList?.id, resetHour]);

const count = counts ? (counts.get(days) ?? 0) : null;

async function handleStart() {
if (!userDb || !sourceList || starting) return;
Expand Down Expand Up @@ -71,15 +86,15 @@ export function SmartReviewModal({ visible, onClose, sourceList }: SmartReviewMo
>
<Text className="text-lg font-semibold text-foreground mb-1">Smart Review</Text>
<Text className="text-sm text-muted-foreground mb-4">
Review cards you flagged on this list, prioritized by how often and how recently you
flagged or failed them.
Review every card you flagged on this list in the window, most-flagged and most-failed
first. Progress here stays separate from the list.
</Text>

<Text className="text-sm font-medium text-muted-foreground mb-2">Lookback window</Text>
<View className="flex-row gap-2 mb-4">
{DAY_OPTIONS.map((d) => (
{WINDOW_OPTIONS.map((d) => (
<Pressable
key={d}
key={windowLabel(d)}
onPress={() => setDays(d)}
className={`flex-1 items-center rounded-lg border py-2 ${
days === d ? "border-primary bg-primary/10" : "border-border"
Expand All @@ -90,7 +105,10 @@ export function SmartReviewModal({ visible, onClose, sourceList }: SmartReviewMo
days === d ? "text-primary" : "text-muted-foreground"
}`}
>
{d}d
{windowLabel(d)}
</Text>
<Text className="text-xs text-muted-foreground">
{counts ? (counts.get(d) ?? 0) : "–"}
</Text>
</Pressable>
))}
Expand All @@ -104,11 +122,11 @@ export function SmartReviewModal({ visible, onClose, sourceList }: SmartReviewMo
</>
) : count === 0 ? (
<Text className="text-sm text-muted-foreground">
No flagged cards in the last {days} days.
No flagged cards {windowPhrase(days)}.
</Text>
) : (
<Text className="text-sm text-foreground">
{count} flagged {count === 1 ? "card" : "cards"} in the last {days} days.
{count} flagged {count === 1 ? "card" : "cards"} {windowPhrase(days)}.
</Text>
)}
</View>
Expand Down
Loading
Loading