Improve analysis UX: algorithm explanations, skeleton loading, auto-expand achieved specs
- Replace terse one-line optimization mode descriptions with clearer multi-sentence explanations of how Maximize Count and Priority Order algorithms behave - Add skeleton loading placeholders on course buttons while analysis is pending - Auto-expand achieved specializations to show credit breakdown by default - Add instructional subtitles to Course Selection and Specializations sections - Make Clear and Clear All buttons more prominent with visible backgrounds
This commit is contained in:
@@ -64,19 +64,17 @@ function ElectiveSet({
|
||||
{!isPinned && hasHighImpact && (
|
||||
<span style={{ fontSize: '11px', color: '#d97706', marginLeft: '8px', fontWeight: 400 }}>high impact</span>
|
||||
)}
|
||||
{!isPinned && loading && !analysis && (
|
||||
<span style={{ fontSize: '11px', color: '#888', marginLeft: '8px', fontWeight: 400 }}>analyzing...</span>
|
||||
)}
|
||||
</h4>
|
||||
{isPinned && (
|
||||
<button
|
||||
onClick={onUnpin}
|
||||
style={{
|
||||
fontSize: '11px', border: 'none', background: 'none',
|
||||
color: '#3b82f6', cursor: 'pointer', padding: '2px 4px',
|
||||
fontSize: '11px', border: '1px solid #bfdbfe', background: '#eff6ff',
|
||||
color: '#2563eb', cursor: 'pointer', padding: '3px 10px',
|
||||
borderRadius: '4px', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
clear
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -89,6 +87,7 @@ function ElectiveSet({
|
||||
{courses.map((course) => {
|
||||
const ceiling = ceilingMap.get(course.id);
|
||||
const reqFor = requiredForSpec[course.id];
|
||||
const showSkeleton = loading && !analysis;
|
||||
return (
|
||||
<button
|
||||
key={course.id}
|
||||
@@ -102,7 +101,19 @@ function ElectiveSet({
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ flex: 1 }}>{course.name}</span>
|
||||
{ceiling && (
|
||||
{showSkeleton ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '60px',
|
||||
height: '14px',
|
||||
borderRadius: '3px',
|
||||
background: 'linear-gradient(90deg, #e5e7eb 25%, #f0f0f0 50%, #e5e7eb 75%)',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: 'skeleton-pulse 1.5s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
) : ceiling ? (
|
||||
<span style={{
|
||||
fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 600,
|
||||
color: ceiling.ceilingCount >= 3 ? '#16a34a' : ceiling.ceilingCount >= 2 ? '#2563eb' : '#666',
|
||||
@@ -114,7 +125,7 @@ function ElectiveSet({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{reqFor && (
|
||||
<span style={{ fontSize: '11px', color: '#92400e', marginTop: '2px' }}>
|
||||
@@ -130,6 +141,8 @@ function ElectiveSet({
|
||||
);
|
||||
}
|
||||
|
||||
const skeletonStyle = `@keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }`;
|
||||
|
||||
export function CourseSelection({ pinnedCourses, treeResults, treeLoading, onPin, onUnpin, onClearAll }: CourseSelectionProps) {
|
||||
const terms: Term[] = ['Spring', 'Summer', 'Fall'];
|
||||
const hasPinned = Object.keys(pinnedCourses).length > 0;
|
||||
@@ -139,14 +152,19 @@ export function CourseSelection({ pinnedCourses, treeResults, treeLoading, onPin
|
||||
|
||||
return (
|
||||
<div>
|
||||
<style>{skeletonStyle}</style>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<h2 style={{ fontSize: '16px', margin: 0 }}>Course Selection</h2>
|
||||
<div>
|
||||
<h2 style={{ fontSize: '16px', margin: 0 }}>Course Selection</h2>
|
||||
<p style={{ fontSize: '12px', color: '#888', margin: '2px 0 0' }}>Select one course per elective slot. Analysis shows how each choice affects your specializations.</p>
|
||||
</div>
|
||||
{hasPinned && (
|
||||
<button
|
||||
onClick={onClearAll}
|
||||
style={{
|
||||
fontSize: '12px', border: 'none', background: 'none',
|
||||
color: '#ef4444', cursor: 'pointer', padding: '2px 6px',
|
||||
fontSize: '12px', border: '1px solid #fecaca', background: '#fef2f2',
|
||||
color: '#dc2626', cursor: 'pointer', padding: '3px 10px',
|
||||
borderRadius: '4px', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
|
||||
@@ -45,10 +45,10 @@ export function ModeToggle({ mode, onSetMode }: ModeToggleProps) {
|
||||
Priority Order
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: '#888', marginTop: '6px' }}>
|
||||
<p style={{ fontSize: '11px', color: '#888', marginTop: '6px', lineHeight: '1.4' }}>
|
||||
{mode === 'maximize-count'
|
||||
? 'Get as many specializations as possible. Ranking breaks ties.'
|
||||
: 'Guarantee your top-ranked specialization first, then add more.'}
|
||||
? 'Finds the combination of specializations that achieves the highest count (up to 3). Your ranking is only used to break ties when multiple combinations achieve the same count.'
|
||||
: 'Processes specializations in your ranked order, top to bottom. Locks in your highest-ranked achievable specialization first, then adds more if feasible. Your ranking directly controls which specializations are prioritized.'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -180,7 +180,23 @@ interface SpecializationRankingProps {
|
||||
}
|
||||
|
||||
export function SpecializationRanking({ ranking, result, onReorder }: SpecializationRankingProps) {
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(result.achieved));
|
||||
const prevAchievedRef = useRef(result.achieved);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevAchievedRef.current;
|
||||
if (prev !== result.achieved) {
|
||||
prevAchievedRef.current = result.achieved;
|
||||
const newlyAchieved = result.achieved.filter((id) => !prev.includes(id));
|
||||
if (newlyAchieved.length > 0) {
|
||||
setExpanded((s) => {
|
||||
const next = new Set(s);
|
||||
for (const id of newlyAchieved) next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [result.achieved]);
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }),
|
||||
@@ -217,7 +233,8 @@ export function SpecializationRanking({ ranking, result, onReorder }: Specializa
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ fontSize: '16px', marginBottom: '8px' }}>Specializations</h2>
|
||||
<h2 style={{ fontSize: '16px', marginBottom: '4px' }}>Specializations</h2>
|
||||
<p style={{ fontSize: '12px', color: '#888', margin: '0 0 8px' }}>Drag or use arrows to rank your preferences. The optimizer uses this order to allocate credits.</p>
|
||||
<div style={{ marginBottom: '8px', fontSize: '13px', color: '#666' }}>
|
||||
{result.achieved.length > 0
|
||||
? `${result.achieved.length} specialization${result.achieved.length > 1 ? 's' : ''} achieved`
|
||||
|
||||
Reference in New Issue
Block a user