Skip to content

[Bug]: Image Occlusion shows Next instead of grading buttons after reveal #142

@JamieAUS

Description

@JamieAUS

Bug Description

Bug Description

When studying an Image Occlusion card, the card reveals correctly, but after revealing the answer the reviewer does not show any grading buttons.

Instead of showing Again / Hard / Good / Easy, it only shows a Next button.

This makes it impossible to manually grade the card from the UI.

Current Behaviour

Before reveal:

  • The image occlusion card is shown with hidden labels/masks.
  • The only main action button is Show Answer.

After pressing Enter / Show Answer:

  • The answer is revealed correctly.
  • But the only main action button becomes Next.
  • No grading buttons appear.
  • Pressing Enter again continues to the next card, without giving me a chance to manually choose a grade.

Screenshots:

  1. Front side: hidden image occlusion labels, Show Answer button visible.
  2. Back side: answer revealed, but only Next button visible. No Again / Hard / Good / Easy.

Settings Already Tried

I tried the related settings and the issue still happens:

  • Studying -> Auto-advance: off
  • Studying -> Auto-advance after: checked
  • Studying -> Skip button: off
  • Studying -> Grading buttons: Four buttons
  • Flashcards -> Image occlusion -> Reveal mode: checked
  • Flashcards -> Hotspot -> Hotspot interaction mode: checked / changed
  • Multiple choice -> Auto-grade: checked, but this is not MCQ
  • Ordered questions -> Auto-grade: checked, but this is not OQ

None of these made the grading buttons appear after revealing the Image Occlusion card.

Code Pointers

From looking at the code, the screenshot appears to match the widget/practice-style footer path.

In the widget render code, the practice footer shows Show Answer, then after reveal it shows only Next:

label: tx(view, "ui.widget.showAnswer", "Show Answer"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
view.showAnswer = true;
view.render();
view.containerEl.focus();
},
kbd: "↵",
});
applyWidgetActionButtonStyles(revealBtn);
footer.appendChild(revealBtn);
} else if (card.type === "oq" && !view.showAnswer) {
const oqSubmitBtn = makeTextButton({
label: tx(view, "ui.widget.oq.submitOrder", "Submit order"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
const oqMap = ensureWidgetOqOrderMap(view.session as unknown as Record<string, unknown>);
const steps = Array.isArray(card.oqSteps) ? card.oqSteps : [];
const currentOrder = oqMap[String(card.id)] || Array.from({ length: steps.length }, (_, i) => i);
void view.answerOq(currentOrder.slice());
},
kbd: "\u21B5",
});
applyWidgetActionButtonStyles(oqSubmitBtn);
footer.appendChild(oqSubmitBtn);
} else if (card.type === "mcq" && !view.showAnswer) {
// MCQ: no footer button on front — user progresses by selecting an option
} else {
const nextBtn = makeTextButton({
label: tx(view, "ui.widget.next", "Next"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => void view.nextCard(),
kbd: "↵",
});
applyWidgetActionButtonStyles(nextBtn);
footer.appendChild(nextBtn);

The scheduled footer, however, seems like it should show grading buttons when showAnswer is true and the card is ungraded:

function renderScheduledFooter(view: WidgetViewLike, footer: HTMLElement, card: CardRecord, graded: { rating: ReviewRating; at: number; meta: ReviewMeta | null } | null, ioLike: boolean) {
// Reveal button (for basic/cloze when hidden)
if ((card.type === "basic" || card.type === "reversed" || card.type === "reversed-child" || card.type === "combo-child" || isClozeLike(card) || ioLike) && !view.showAnswer && !graded) {
const revealBtn = makeTextButton({
label: tx(view, "ui.widget.revealAnswer", "Reveal Answer"),
title: "Reveal answer",
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
view.showAnswer = true;
view.render();
view.containerEl.focus();
},
kbd: "↵",
});
applyWidgetActionButtonStyles(revealBtn);
footer.appendChild(revealBtn);
}
// Grading buttons row – 2×2 grid layout (Again+Hard, Good+Easy)
if (!graded) {
if ((card.type === "basic" || card.type === "reversed" || card.type === "reversed-child" || card.type === "combo-child" || isClozeLike(card) || ioLike) && view.showAnswer) {
const fourButton = !!view.plugin.settings.study.fourButtonMode;
const showIntervals = !!view.plugin.settings.study.showGradeIntervals;
const previewNow = Date.now();
const previewState = showIntervals
? view.plugin.store.ensureState(String(card.id), previewNow)
: null;
const getSubtitle = (rating: ReviewRating): string | undefined => {
if (!previewState || !showIntervals) return undefined;
return (
getRatingIntervalPreview({
state: previewState,
rating,
now: previewNow,
scheduling: view.plugin.settings.scheduling,
}) ?? undefined
);
};
let gradingGrid: HTMLElement;
if (fourButton) {
gradingGrid = el("div", "sprout-widget-grading-grid");
} else {
gradingGrid = el("div", "sprout-widget-grading-row");
}
// Always show Again
const againBtn = makeTextButton({
label: tx(view, "ui.widget.grade.again", "Again"),
subtitle: getSubtitle("again"),
title: tx(view, "ui.widget.grade.againTooltip", "Not recalled easily (1)"),
className: fourButton
? "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full"
: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-half",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("again", {});
view.render();
})();
},
kbd: fourButton ? "1" : "1",
});
applyWidgetActionButtonStyles(againBtn);
gradingGrid.appendChild(againBtn);
if (fourButton) {
const hardBtn = makeTextButton({
label: tx(view, "ui.widget.grade.hard", "Hard"),
subtitle: getSubtitle("hard"),
title: tx(view, "ui.widget.grade.hardTooltip", "Recalled with difficulty (2)"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("hard", {});
view.render();
})();
},
kbd: "2",
});
applyWidgetActionButtonStyles(hardBtn);
gradingGrid.appendChild(hardBtn);
}
// Always show Good
const goodBtn = makeTextButton({
label: tx(view, "ui.widget.grade.good", "Good"),
subtitle: getSubtitle("good"),
title: fourButton
? tx(view, "ui.widget.grade.goodTooltipFour", "Recalled with effort (3)")
: tx(view, "ui.widget.grade.goodTooltipTwo", "Recalled easily (2)"),
className: fourButton
? "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full"
: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-half",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("good", {});
view.render();
})();
},
kbd: fourButton ? "3" : "2",
});
applyWidgetActionButtonStyles(goodBtn);
gradingGrid.appendChild(goodBtn);
if (fourButton) {
const easyBtn = makeTextButton({
label: tx(view, "ui.widget.grade.easy", "Easy"),
subtitle: getSubtitle("easy"),
title: tx(view, "ui.widget.grade.easyTooltip", "Recalled easily (4)"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("easy", {});
view.render();
})();
},
kbd: "4",
});
applyWidgetActionButtonStyles(easyBtn);
gradingGrid.appendChild(easyBtn);
}
footer.appendChild(gradingGrid);
} else if (card.type === "mcq") {
const mcqNote = el("div", "text-muted-foreground w-full text-center sprout-widget-info");
mcqNote.textContent = isMultiAnswerMcq(card)
? tx(view, "ui.widget.mcq.selectAllThenSubmit", "Select all correct answers, then submit")
: tx(view, "ui.widget.mcq.selectOption", "Select an option");
footer.appendChild(mcqNote);
} else if (card.type === "oq") {
const oqSubmitBtn = makeTextButton({
label: tx(view, "ui.widget.oq.submitOrder", "Submit order"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
const oqMap = ensureWidgetOqOrderMap(view.session as unknown as Record<string, unknown>);
const steps = Array.isArray(card.oqSteps) ? card.oqSteps : [];
const currentOrder = oqMap[String(card.id)] || Array.from({ length: steps.length }, (_, i) => i);
void view.answerOq(currentOrder.slice());
},
kbd: "\u21B5",
});
applyWidgetActionButtonStyles(oqSubmitBtn);
footer.appendChild(oqSubmitBtn);
}
} else {
const nextBtn = makeTextButton({
label: tx(view, "ui.widget.next", "Next"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => void view.nextCard(),
kbd: "↵",
});
applyWidgetActionButtonStyles(nextBtn);
footer.appendChild(nextBtn);
}

Specifically, this part looks like the expected grading path:

// Grading buttons row – 2×2 grid layout (Again+Hard, Good+Easy)
if (!graded) {
if ((card.type === "basic" || card.type === "reversed" || card.type === "reversed-child" || card.type === "combo-child" || isClozeLike(card) || ioLike) && view.showAnswer) {
const fourButton = !!view.plugin.settings.study.fourButtonMode;
const showIntervals = !!view.plugin.settings.study.showGradeIntervals;
const previewNow = Date.now();
const previewState = showIntervals
? view.plugin.store.ensureState(String(card.id), previewNow)
: null;
const getSubtitle = (rating: ReviewRating): string | undefined => {
if (!previewState || !showIntervals) return undefined;
return (
getRatingIntervalPreview({
state: previewState,
rating,
now: previewNow,
scheduling: view.plugin.settings.scheduling,
}) ?? undefined
);
};
let gradingGrid: HTMLElement;
if (fourButton) {
gradingGrid = el("div", "sprout-widget-grading-grid");
} else {
gradingGrid = el("div", "sprout-widget-grading-row");
}
// Always show Again
const againBtn = makeTextButton({
label: tx(view, "ui.widget.grade.again", "Again"),
subtitle: getSubtitle("again"),
title: tx(view, "ui.widget.grade.againTooltip", "Not recalled easily (1)"),
className: fourButton
? "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full"
: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-half",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("again", {});
view.render();
})();
},
kbd: fourButton ? "1" : "1",
});
applyWidgetActionButtonStyles(againBtn);
gradingGrid.appendChild(againBtn);
if (fourButton) {
const hardBtn = makeTextButton({
label: tx(view, "ui.widget.grade.hard", "Hard"),
subtitle: getSubtitle("hard"),
title: tx(view, "ui.widget.grade.hardTooltip", "Recalled with difficulty (2)"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("hard", {});
view.render();
})();
},
kbd: "2",
});
applyWidgetActionButtonStyles(hardBtn);
gradingGrid.appendChild(hardBtn);
}
// Always show Good
const goodBtn = makeTextButton({
label: tx(view, "ui.widget.grade.good", "Good"),
subtitle: getSubtitle("good"),
title: fourButton
? tx(view, "ui.widget.grade.goodTooltipFour", "Recalled with effort (3)")
: tx(view, "ui.widget.grade.goodTooltipTwo", "Recalled easily (2)"),
className: fourButton
? "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full"
: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-half",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("good", {});
view.render();
})();
},
kbd: fourButton ? "3" : "2",
});
applyWidgetActionButtonStyles(goodBtn);
gradingGrid.appendChild(goodBtn);
if (fourButton) {
const easyBtn = makeTextButton({
label: tx(view, "ui.widget.grade.easy", "Easy"),
subtitle: getSubtitle("easy"),
title: tx(view, "ui.widget.grade.easyTooltip", "Recalled easily (4)"),
className: "learnkit-btn-toolbar sprout-widget-btn sprout-widget-btn-full",
onClick: () => {
void (async () => {
await view.gradeCurrentRating("easy", {});
view.render();
})();
},
kbd: "4",
});
applyWidgetActionButtonStyles(easyBtn);
gradingGrid.appendChild(easyBtn);
}
footer.appendChild(gradingGrid);

So it looks like the Image Occlusion review is either:

  1. being treated as practice/no-scheduling mode, or
  2. being considered already graded after reveal, or
  3. taking the wrong footer/render path for this card/session.

There is also code where pressing Next can auto-grade an ungraded scheduled card as again:

if (card) {
const id = String(card.id);
if (!view.session.graded[id]) {
// Auto-grade unanswered as AGAIN
await gradeCurrentRating(view, "again", { auto: true, via: "next" });
}

So the visible problem is: after reveal, the UI only offers Next, not manual grading.

Suggested Fix Direction

For Image Occlusion cards in a normal study/review session, after the answer is revealed and the card is still ungraded, the UI should render the standard grading buttons instead of Next.

If this is happening because the card/session is in practice mode, the UI should make that clearer, because from the user side it looks like a normal study session but grading is unavailable.

Steps to Reproduce

  1. Create or open an Image Occlusion card.
  2. Start studying it from the current study UI/widget/reviewer flow.
  3. On the front side, confirm that the image labels/masks are hidden and the main button says Show Answer.
  4. Press Enter or click Show Answer.
  5. The answer is revealed.
  6. Observe that the footer shows only Next, not the manual grading buttons.
  7. Pressing Enter again advances to the next card without allowing manual grading.
Image Image

Expected Behavior

Expected Behaviour

After revealing an Image Occlusion card, LearnKit should show the grading buttons:

  • Again
  • Hard
  • Good
  • Easy

Then the user should manually choose a grade before the card advances.

Expected flow:

  1. Hidden answer + Enter = reveal answer
  2. Revealed answer = show grading buttons
  3. User presses 1, 2, 3, or 4
  4. Card is graded and then advances

LearnKit Version

1.3.3

Obsidian Version

1.12.7

Operating System

macOS

Additional Context

Screenshots show the exact UI state: before reveal, the IO card has hidden labels and a Show Answer button; after reveal, the answer appears but the footer only shows Next, with no manual grading buttons.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions