v1.5.1: Show completed plan in Top Plans

Once every elective set is pinned, the Top Plans panel now renders the
user's completed selection as a single "Your Plan" card showing the
achievement count and pinned courses. Previously the panel went blank
because the all-pinned branch in useAppState cleared topPlans, even
though the spec strip was already showing the achievement.

Synthesizes a single PlanOutcome from pinnedAssignments +
optimizationResult.achieved + the priority scorer; TopPlans detects the
all-pinned state via ELECTIVE_SETS.every(...) and bypasses the
length>0 filter so a 0-spec completed plan still renders honestly.
This commit is contained in:
2026-05-10 11:57:56 -04:00
parent 3a5ebaa17a
commit cb2024f857
10 changed files with 246 additions and 5 deletions
+5 -3
View File
@@ -38,7 +38,9 @@ function formatScore(n: number): string {
export function TopPlans({ plans, partial, loading, progress, pinnedCourses, ranking, showAnimatedBar = true, onAdopt, onPin, onUnpin }: TopPlansProps) {
const rankWeight = useMemo(() => makePriorityRankWeight(ranking), [ranking]);
const visible = plans.filter((p) => p.achievedSpecs.length > 0);
const allPinned = ELECTIVE_SETS.every((s) => pinnedCourses[s.id]);
const visible = allPinned ? plans : plans.filter((p) => p.achievedSpecs.length > 0);
const heading = allPinned ? 'Your Plan' : 'Top Plans';
const pct = progress && progress.iterationsTotal > 0
? Math.min(100, (progress.iterations / progress.iterationsTotal) * 100)
@@ -55,8 +57,8 @@ export function TopPlans({ plans, partial, loading, progress, pinnedCourses, ran
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '8px', gap: '8px', flexWrap: 'wrap' }}>
<h3 style={{ fontSize: '14px', margin: 0, color: '#444' }}>
Top Plans
{visible.length > 0 && (
{heading}
{!allPinned && visible.length > 0 && (
<span style={{ fontSize: '11px', color: '#888', fontWeight: 400, marginLeft: '6px' }}>
ranked by specs achieved
</span>
+10 -1
View File
@@ -11,6 +11,7 @@ import {
selectPriorityTarget,
} from '../solver/decisionTree';
import { computeUpperBounds } from '../solver/feasibility';
import { makePriorityScorer } from '../solver/priority';
import type { SetAnalysis, PlanOutcome } from '../solver/decisionTree';
import type { WorkerRequest, WorkerResponse } from '../workers/decisionTree.worker';
import { cancelledCourseIds, courseIdsByName, courseById } from '../data/lookups';
@@ -183,8 +184,16 @@ export function useAppState() {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (openSetIds.length === 0) {
// Selection is complete — synthesize a single PlanOutcome from the
// pinned assignments so Top Plans can render the user's completed plan.
const scorer = makePriorityScorer(state.ranking);
const completed: PlanOutcome = {
courseAssignments: pinnedAssignments,
achievedSpecs: optimizationResult.achieved,
priorityScore: scorer(optimizationResult.achieved),
};
setTreeResults([]);
setTopPlans([]);
setTopPlans([completed]);
setTopPlansPartial(false);
setSearchProgress(null);
setTreeLoading(false);
+1 -1
View File
@@ -6,7 +6,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
define: {
__APP_VERSION__: JSON.stringify('1.5.0'),
__APP_VERSION__: JSON.stringify('1.5.1'),
__APP_VERSION_DATE__: JSON.stringify('2026-05-10'),
},
server: {