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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Enhanced Skip Functionality with Prerequisite Finding**
- New skip reason selection UI when skipping a problem (Too difficult, Don't understand, Not relevant, Other)
- "Don't understand" skips now find and replace with easier prerequisite problems
- "Too difficult" skips weaken problem graph relationships for future recommendations
- "Free skip" detection: problems with no relationships to attempted problems have no graph penalty
- Smart prerequisite search prioritizes problems by difficulty (Easy > Medium > Hard) and connection strength

- **Guard Rail 4: Poor Performance Protection**
- New session composition safety for users promoted via stagnation escape hatch
- When at Hard cap with <50% recent accuracy: limits Hard problems to 1 per session
- Replaces excess Hard problems with Medium to prevent overwhelming struggling users
- Tracks promotion type (standard_volume_gate vs stagnation_escape_hatch) for intelligent session balancing

- **Enhanced "I'm Stuck" Button** (#234)
- Now extends timer by 5 minutes when clicked
- Automatically opens hints panel to help user get unstuck
- Records user intent for session analytics

### Fixed
- **Native Dropdown Broken in Chrome Extension Content Scripts**
- Fixed dropdown menus (select elements) flashing and immediately closing when clicked on Problem Submission page
- Root cause: Chrome browser update changed how native `<select>` elements work in extension content scripts
- Created `SimpleSelect.jsx` custom dropdown component that renders within React DOM instead of using browser-native UI
- Custom dropdown includes full keyboard navigation (Arrow keys, Enter, Space, Escape), click-outside-to-close, and maintains visual consistency
- Changed CSS containment from `contain: layout size style` to `contain: paint style` on `.cm-sidenav` in main.css
- Changed CSS containment from `contain: layout` to `contain: paint style` in probrec.css (2 instances)
- CSS containment with `layout` was preventing dropdowns from rendering outside their container bounds
- Maintains compatibility with react-hook-form and all existing functionality

- **Problem List Not Updating After Submission**
- Fixed problems not being removed from generator list after submitting a solution
- Root cause: Sidebar unmounting fix (Dec 23) prevented natural data refresh on remount
Expand Down Expand Up @@ -58,6 +81,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated CLAUDE.md with theming guidelines

### Tests
- Added Guard Rail 4 unit tests for poor performance protection trigger conditions
- Added escape hatch promotion type tracking tests (standard_volume_gate vs stagnation_escape_hatch)
- Added backward compatibility tests for Guard Rails 1-3
- Added regression test for timer settings cache clearing
- Added regression tests for cross-context theme sync
- Added comprehensive tests for `ProblemGeneratorHooks` message listeners
Expand Down
5 changes: 2 additions & 3 deletions chrome-extension-app/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "CodeMaster - Algorithm Learning Assistant",
"version": "1.0.0",
"version": "1.1.0",
"description": "Master algorithms with personalized spaced repetition and pattern ladders. Track your LeetCode progress with smart analytics.",
"manifest_version": 3,
"homepage_url": "https://github.com/smithrashell/CodeMaster",
Expand Down Expand Up @@ -46,8 +46,7 @@
"run_at": "document_idle",
"js": [
"content.js"
],
"media": []
]
}
],
"web_accessible_resources": [
Expand Down
97 changes: 92 additions & 5 deletions chrome-extension-app/src/background/handlers/problemHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
import { ProblemService } from "../../shared/services/problem/problemService.js";
import { AttemptsService } from "../../shared/services/attempts/attemptsService.js";
import { getProblemWithOfficialDifficulty } from "../../shared/db/stores/problems.js";
import {
weakenRelationshipsForSkip,
hasRelationshipsToAttempted,
findPrerequisiteProblem
} from "../../shared/db/stores/problem_relationships.js";
import { SessionService } from "../../shared/services/session/sessionService.js";
import { getLatestSession } from "../../shared/db/stores/sessions.js";

/**
* Handler: getProblemByDescription
Expand Down Expand Up @@ -113,13 +120,93 @@ export function handleProblemSubmitted(request, dependencies, sendResponse, fini

/**
* Handler: skipProblem
* Acknowledges problem skip request
* Handles problem skip with reason-based behavior
*
* Skip reasons and their actions:
* - "too_difficult": Weaken graph relationships (if problem has connections)
* - "dont_understand": Find prerequisite problem as replacement
* - "not_relevant": Just remove from session
* - "other": Just remove from session
*
* Free skip: Problems with zero relationships to attempted problems get no graph penalty
*/
export function handleSkipProblem(request, dependencies, sendResponse, finishRequest) {
console.log("⏭️ Skipping problem:", request.consentScriptData?.leetcode_id || "unknown");
// Acknowledge the skip request - no additional processing needed
sendResponse({ message: "Problem skipped successfully" });
finishRequest();
const leetcodeId = request.leetcodeId || request.problemData?.leetcode_id || request.consentScriptData?.leetcode_id;
const VALID_SKIP_REASONS = ['too_difficult', 'dont_understand', 'not_relevant', 'other'];
const skipReason = VALID_SKIP_REASONS.includes(request.skipReason) ? request.skipReason : 'other';

// Input validation
if (!leetcodeId) {
console.error("❌ skipProblem called without valid leetcodeId");
sendResponse({ error: "Invalid problem ID", message: "Problem skip failed" });
finishRequest();
return true;
}

console.log(`⏭️ Skipping problem ${leetcodeId} - Reason: ${skipReason}`);

(async () => {
try {
const result = {
message: "Problem skipped successfully",
skipReason,
prerequisite: null,
graphUpdated: false,
freeSkip: false
};

// Check if this is a "free skip" (no relationships to attempted problems)
const hasRelationships = await hasRelationshipsToAttempted(leetcodeId);
result.freeSkip = !hasRelationships;

// Handle based on skip reason
if (skipReason === 'too_difficult') {
// Weaken graph relationships for "too difficult" skips (if has relationships)
if (hasRelationships) {
const weakenResult = await weakenRelationshipsForSkip(leetcodeId);
result.graphUpdated = weakenResult.updated > 0;
console.log(`📉 Graph updated: ${weakenResult.updated} relationships weakened`);
}
// Always remove from session
await SessionService.skipProblem(leetcodeId);
console.log(`✅ Removed problem ${leetcodeId} from session`);
} else if (skipReason === 'dont_understand') {
// Find prerequisite problem for "don't understand" skips
// Get current session to exclude problems already in it
const session = await getLatestSession();
const excludeIds = session?.problems?.map(p => p.leetcode_id) || [];

const prerequisite = await findPrerequisiteProblem(leetcodeId, excludeIds);
if (prerequisite) {
result.prerequisite = prerequisite;
result.replaced = true;
console.log(`🎓 Found prerequisite: ${prerequisite.title}`);
// Remove skipped problem and add prerequisite as replacement
await SessionService.skipProblem(leetcodeId, prerequisite);
console.log(`✅ Replaced problem ${leetcodeId} with prerequisite ${prerequisite.leetcode_id || prerequisite.id}`);
} else {
// No prerequisite found, just remove
await SessionService.skipProblem(leetcodeId);
console.log(`✅ Removed problem ${leetcodeId} from session (no prerequisite found)`);
}
} else {
// "not_relevant" or "other" - just remove from session
await SessionService.skipProblem(leetcodeId);
console.log(`✅ Removed problem ${leetcodeId} from session`);
}

sendResponse(result);
} catch (error) {
console.error("❌ Error in skipProblem handler:", error);
sendResponse({
message: "Problem skipped with errors",
error: error.message
});
} finally {
finishRequest();
}
})();

return true;
}

Expand Down
14 changes: 14 additions & 0 deletions chrome-extension-app/src/content/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Main, { Menubutton } from "./features/navigation/main";
import ProbGen from "./features/problems/ProblemGenerator";
import ProbTime from "./features/problems/ProblemTime";
import SkipReason from "./features/problems/SkipReason";
import Settings from "./features/settings/settings";
import TimerBanner from "./components/timer/timercomponent";
// Removed Mantine CSS import - not needed in content script
Expand Down Expand Up @@ -176,7 +177,7 @@
);
};

const Router = () => {

Check warning on line 180 in chrome-extension-app/src/content/App.jsx

View workflow job for this annotation

GitHub Actions / lint-and-test

Arrow function has too many lines (103). Maximum allowed is 100

Check warning on line 180 in chrome-extension-app/src/content/App.jsx

View workflow job for this annotation

GitHub Actions / chrome-extension-tests (18.x)

Arrow function has too many lines (103). Maximum allowed is 100

Check warning on line 180 in chrome-extension-app/src/content/App.jsx

View workflow job for this annotation

GitHub Actions / chrome-extension-tests (20.x)

Arrow function has too many lines (103). Maximum allowed is 100
return (
<ErrorBoundary
section="Content Script Application"
Expand Down Expand Up @@ -252,6 +253,19 @@
</div>
}
/>
<Route
path="/SkipReason"
element={
<div className="main-content">
<ErrorBoundary
section="Skip Reason"
fallback={GenericErrorFallback}
>
<SkipReason />
</ErrorBoundary>
</div>
}
/>
<Route
path="/Timer"
element={
Expand Down
7 changes: 3 additions & 4 deletions chrome-extension-app/src/content/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,9 @@ body[data-theme="light"] .cm-extension #cm-menuButton:hover {
);
height: 100vh;
max-height: 100vh;
/* Prevent affecting page layout */
contain: layout size style;
/* Ensure sidebar doesn't compete with dropdown z-index */
z-index: 9000 !important;
/* Prevent affecting page layout - use paint/style containment only
Note: 'layout' containment can break native <select> dropdowns */
contain: paint style;
padding: var(--cm-content-padding);
background-color: var(--cm-bg);
border: 2px solid var(--cm-bg);
Expand Down
143 changes: 140 additions & 3 deletions chrome-extension-app/src/content/css/probrec.css
Original file line number Diff line number Diff line change
Expand Up @@ -1701,7 +1701,8 @@ html body .cm-extension .cd-sidenav.problem-sidebar-view .cd-sidenav__content {
scroll-behavior: smooth !important;
/* Ensure content can't escape container */
position: relative !important;
contain: layout !important;
/* Use paint/style containment only - layout containment breaks native <select> dropdowns */
contain: paint style !important;
}

/* Override height constraints when strategy map content is expanded */
Expand Down Expand Up @@ -1736,8 +1737,8 @@ html body .cm-extension .cm-sidenav.problem-sidebar-view,
html body .cm-extension .cd-sidenav.problem-sidebar-view,
html body .problem-sidebar {
overflow-y: visible !important;
/* Ensure parent allows child to handle overflow */
contain: layout !important;
/* Use paint/style containment only - layout containment breaks native <select> dropdowns */
contain: paint style !important;
}

/* COPY EXISTING APP SCROLLBAR STYLING FOR PROBLEM SIDEBAR */
Expand Down Expand Up @@ -2435,4 +2436,140 @@ button.tag-strategy-button-expanded,
box-sizing: border-box !important;
}

/* ============================================================================
SKIP REASON PAGE STYLES
============================================================================ */

.cm-extension .skip-reason-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 8px 0;
}

.cm-extension .skip-reason-prompt {
font-size: 14px;
color: var(--cm-text);
margin: 0;
line-height: 1.5;
}

.cm-extension .skip-reason-prompt strong {
color: var(--cm-text);
}

.cm-extension .skip-reason-options {
display: flex;
flex-direction: column;
gap: 8px;
}

.cm-extension .skip-reason-option {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border: 1px solid var(--cm-border);
border-radius: 8px;
background-color: var(--cm-card-bg);
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
width: 100%;
}

.cm-extension .skip-reason-option:hover {
border-color: var(--cm-primary);
background-color: var(--cm-bg-secondary);
}

.cm-extension .skip-reason-option.selected {
border-color: var(--cm-primary);
background-color: var(--cm-accent-bg);
}

.cm-extension .skip-reason-emoji {
font-size: 20px;
line-height: 1;
flex-shrink: 0;
}

.cm-extension .skip-reason-text {
display: flex;
flex-direction: column;
gap: 2px;
}

.cm-extension .skip-reason-label {
font-size: 14px;
font-weight: 500;
color: var(--cm-text);
}

.cm-extension .skip-reason-description {
font-size: 12px;
color: var(--cm-text-dimmed);
}

.cm-extension .skip-reason-other-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--cm-border);
border-radius: 6px;
background-color: var(--cm-card-bg);
color: var(--cm-text);
font-size: 13px;
font-family: inherit;
resize: vertical;
min-height: 60px;
}

.cm-extension .skip-reason-other-input:focus {
outline: none;
border-color: var(--cm-primary);
}

.cm-extension .skip-reason-other-input::placeholder {
color: var(--cm-text-dimmed);
}

.cm-extension .skip-reason-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}

.cm-extension .skip-reason-cancel-btn {
flex: 1;
}

.cm-extension .skip-reason-submit-btn {
flex: 2;
}

.cm-extension .skip-reason-submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.cm-extension .skip-reason-hint {
font-size: 12px;
color: var(--cm-text-dimmed);
background-color: var(--cm-accent-bg);
padding: 10px 12px;
border-radius: 6px;
margin: 0;
line-height: 1.4;
}

.cm-extension .skip-reason-error {
font-size: 13px;
color: var(--cm-status-hard);
background-color: rgba(239, 68, 68, 0.1);
padding: 10px 12px;
border-radius: 6px;
margin: 0;
line-height: 1.4;
}


Loading
Loading