ieeeColors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
red: "#c0392b",
green: "#27ae60",
blue: "#3498db",
yellow: "#f1c40f"
})
// UX Heuristics reference
heuristics = [
{ id: "h1", name: "Visibility of System Status", category: "Nielsen" },
{ id: "h2", name: "Match Real World", category: "Nielsen" },
{ id: "h3", name: "User Control & Freedom", category: "Nielsen" },
{ id: "h4", name: "Consistency & Standards", category: "Nielsen" },
{ id: "h5", name: "Error Prevention", category: "Nielsen" },
{ id: "h6", name: "Recognition vs Recall", category: "Nielsen" },
{ id: "h7", name: "Flexibility & Efficiency", category: "Nielsen" },
{ id: "h8", name: "Aesthetic & Minimalist Design", category: "Nielsen" },
{ id: "h9", name: "Error Recovery", category: "Nielsen" },
{ id: "h10", name: "Help & Documentation", category: "Nielsen" },
{ id: "iot1", name: "Feedback Latency", category: "IoT-Specific" },
{ id: "iot2", name: "Connectivity Status", category: "IoT-Specific" },
{ id: "iot3", name: "Multi-Device Consistency", category: "IoT-Specific" },
{ id: "wcag1", name: "Accessibility (Color Contrast)", category: "WCAG" },
{ id: "wcag2", name: "Accessibility (Touch Targets)", category: "WCAG" }
]
// Interface scenarios by difficulty
scenarios = ({
beginner: [
{
id: 1,
title: "Smart Thermostat Control",
context: "A smart thermostat interface for controlling home temperature",
mockup: {
type: "thermostat",
elements: [
"┌─────────────────────┐",
"│ Smart Thermostat │",
"├─────────────────────┤",
"│ │",
"│ 72° │",
"│ [+] [-] │",
"│ │",
"│ [Mode: ???] │",
"│ │",
"└─────────────────────┘"
]
},
description: "User sees current temperature (72°) with + and - buttons, but the current mode shows as '???'. There's no indication if the system is heating, cooling, or off.",
violations: [
{
id: "v1",
heuristic: "h1",
name: "Visibility of System Status",
description: "Mode shows '???' - user can't tell if heating/cooling/off",
isViolation: true
},
{
id: "v2",
heuristic: "h6",
name: "Recognition vs Recall",
description: "No visual indication of what + and - do (heat up/cool down unclear)",
isViolation: true
},
{
id: "v3",
heuristic: "iot2",
name: "Connectivity Status",
description: "No indicator showing if device is connected to WiFi/hub",
isViolation: true
},
{
id: "v4",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Interface is clean and uncluttered",
isViolation: false
},
{
id: "v5",
heuristic: "wcag2",
name: "Touch Targets",
description: "Buttons appear to be adequate size for touch",
isViolation: false
}
],
hint: "Think about what information is missing that users need to understand the device's current state.",
expectedCount: 3
},
{
id: 2,
title: "Smart Light Bulb App",
context: "Mobile app for controlling smart RGB light bulbs",
mockup: {
type: "mobile-app",
elements: [
"┌────────────────┐",
"│ Lights │",
"├────────────────┤",
"│ │",
"│ Color: #FF5733 │",
"│ │",
"│ [Apply] │",
"│ │",
"└────────────────┘"
]
},
description: "User enters hex color code #FF5733 and clicks Apply. No preview of the color is shown before applying, and there's no way to undo if they made a mistake.",
violations: [
{
id: "v1",
heuristic: "h6",
name: "Recognition vs Recall",
description: "Requires memorizing hex codes instead of color picker",
isViolation: true
},
{
id: "v2",
heuristic: "h3",
name: "User Control & Freedom",
description: "No undo button if wrong color is applied",
isViolation: true
},
{
id: "v3",
heuristic: "h5",
name: "Error Prevention",
description: "No color preview before applying",
isViolation: true
},
{
id: "v4",
heuristic: "iot1",
name: "Feedback Latency",
description: "Immediate feedback when Apply is clicked",
isViolation: false
},
{
id: "v5",
heuristic: "wcag1",
name: "Color Contrast",
description: "Text appears readable against background",
isViolation: false
}
],
hint: "Consider how users typically pick colors and what happens if they make mistakes.",
expectedCount: 3
},
{
id: 3,
title: "Smart Lock Status Screen",
context: "Display showing smart door lock status",
mockup: {
type: "lock-display",
elements: [
"┌──────────────────┐",
"│ Door Lock │",
"├──────────────────┤",
"│ │",
"│ Status: 1 │",
"│ │",
"│ [Toggle] │",
"│ │",
"└──────────────────┘"
]
},
description: "Status shows '1' (presumably locked), but uses a code instead of clear language. The Toggle button doesn't indicate what it will do.",
violations: [
{
id: "v1",
heuristic: "h2",
name: "Match Real World",
description: "Uses '1' instead of 'Locked' or 'Unlocked' - not user language",
isViolation: true
},
{
id: "v2",
heuristic: "h6",
name: "Recognition vs Recall",
description: "User must remember what '1' means (locked vs unlocked)",
isViolation: true
},
{
id: "v3",
heuristic: "h5",
name: "Error Prevention",
description: "Toggle button doesn't show what action it will perform",
isViolation: true
},
{
id: "v4",
heuristic: "h4",
name: "Consistency & Standards",
description: "Universal toggle pattern is recognizable",
isViolation: false
}
],
hint: "Think about whether users can understand the interface without prior training.",
expectedCount: 3
},
{
id: 4,
title: "Smart Plug Scheduling",
context: "Interface for scheduling when a smart plug turns on/off",
mockup: {
type: "schedule",
elements: [
"┌────────────────────┐",
"│ Schedule │",
"├────────────────────┤",
"│ On: 0700 │",
"│ Off: 2200 │",
"│ │",
"│ [Save Changes] │",
"│ │",
"└────────────────────┘"
]
},
description: "User sets schedule using 24-hour time (0700, 2200). After clicking Save Changes, there's no confirmation if it worked. No way to see existing schedules or edit them.",
violations: [
{
id: "v1",
heuristic: "h2",
name: "Match Real World",
description: "24-hour time format not familiar to all users (should offer 12-hour option)",
isViolation: true
},
{
id: "v2",
heuristic: "iot1",
name: "Feedback Latency",
description: "No confirmation message after saving",
isViolation: true
},
{
id: "v3",
heuristic: "h3",
name: "User Control & Freedom",
description: "No way shown to view or edit existing schedules",
isViolation: true
},
{
id: "v4",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Layout is clean and focused",
isViolation: false
},
{
id: "v5",
heuristic: "wcag2",
name: "Touch Targets",
description: "Save button appears appropriately sized",
isViolation: false
}
],
hint: "Consider feedback and flexibility - does the user know what happened and can they manage their schedules?",
expectedCount: 3
}
],
intermediate: [
{
id: 5,
title: "Wearable Device Pairing",
context: "Bluetooth pairing flow for fitness tracker",
mockup: {
type: "pairing",
elements: [
"┌──────────────────────┐",
"│ Pair Device │",
"├──────────────────────┤",
"│ │",
"│ Searching... │",
"│ │",
"│ Error: BT_CONN_ERR │",
"│ 0x8007 │",
"│ │",
"│ [Try Again] [Exit] │",
"│ │",
"└──────────────────────┘"
]
},
description: "Pairing failed with cryptic error 'BT_CONN_ERR 0x8007'. User is stuck with only 'Try Again' or 'Exit' - no guidance on fixing the issue. Progress indicator doesn't show how far the pairing process got.",
violations: [
{
id: "v1",
heuristic: "h9",
name: "Error Recovery",
description: "Error code 'BT_CONN_ERR 0x8007' is technical, not user-friendly",
isViolation: true
},
{
id: "v2",
heuristic: "h9",
name: "Error Recovery",
description: "No actionable guidance on how to fix the Bluetooth error",
isViolation: true
},
{
id: "v3",
heuristic: "h1",
name: "Visibility of System Status",
description: "'Searching...' doesn't indicate progress or how long it will take",
isViolation: true
},
{
id: "v4",
heuristic: "iot3",
name: "Multi-Device Consistency",
description: "No indication if issue is with phone, tracker, or both",
isViolation: true
},
{
id: "v5",
heuristic: "h10",
name: "Help & Documentation",
description: "No help link or troubleshooting steps",
isViolation: true
},
{
id: "v6",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "Try Again button provides quick retry option",
isViolation: false
}
],
hint: "Focus on error messages and what information users need when things go wrong.",
expectedCount: 5
},
{
id: 6,
title: "Industrial HMI Panel",
context: "Factory floor Human-Machine Interface for motor control",
mockup: {
type: "industrial",
elements: [
"┌────────────────────────────┐",
"│ Motor Control Panel │",
"├────────────────────────────┤",
"│ MTR_01: RPM=3450 T=87C │",
"│ MTR_02: RPM=0 T=23C │",
"│ MTR_03: RPM=2200 T=FAULT │",
"│ MTR_04: RPM=1800 T=45C │",
"│ │",
"│ [START_ALL] [STOP_ALL] │",
"│ [EMERG_STOP] │",
"└────────────────────────────┘"
]
},
description: "Displays 4 motors with IDs like 'MTR_01', 'MTR_03' (requires memorization). MTR_03 shows 'T=FAULT' but no explanation. EMERG_STOP button is same size/color as regular buttons. Dense information with abbreviations (RPM, T).",
violations: [
{
id: "v1",
heuristic: "h6",
name: "Recognition vs Recall",
description: "Motor IDs like MTR_01, MTR_03 require memorization - should have descriptive names",
isViolation: true
},
{
id: "v2",
heuristic: "h9",
name: "Error Recovery",
description: "MTR_03 shows T=FAULT but no explanation of what's wrong",
isViolation: true
},
{
id: "v3",
heuristic: "h5",
name: "Error Prevention",
description: "EMERG_STOP not visually distinct - critical action should stand out",
isViolation: true
},
{
id: "v4",
heuristic: "wcag1",
name: "Color Contrast",
description: "All text appears same color/size - no visual hierarchy for alarms",
isViolation: true
},
{
id: "v5",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Cluttered with abbreviations (RPM, T, MTR) - not beginner-friendly",
isViolation: true
},
{
id: "v6",
heuristic: "h4",
name: "Consistency & Standards",
description: "Standard industrial abbreviations like RPM are acceptable",
isViolation: false
}
],
hint: "Consider safety-critical aspects and whether operators can quickly understand motor status.",
expectedCount: 5
},
{
id: 7,
title: "Smart Home Dashboard",
context: "Central dashboard for managing all smart home devices",
mockup: {
type: "dashboard",
elements: [
"┌─────────────────────────────────────┐",
"│ Home Dashboard [Settings] │",
"├─────────────────────────────────────┤",
"│ │",
"│ Living Room Light │ Thermostat │",
"│ On │ 75% │ #FFF │ 72° │ Cooling │",
"│ │",
"│ Front Door Lock │ Garage Door │",
"│ 1 │ 14:32:05 │ 0 │ 08:15:22 │",
"│ │",
"│ Kitchen Camera │ Bedroom Speaker │",
"│ REC │ 2.4Mbps │ VOL=45 │ BT │",
"│ │",
"└─────────────────────────────────────┘"
]
},
description: "Dashboard shows all devices with inconsistent status formats: Light uses hex color, locks use cryptic codes (1=locked?), camera shows technical specs (Mbps), timestamps without context. Mix of terminology and units.",
violations: [
{
id: "v1",
heuristic: "h4",
name: "Consistency & Standards",
description: "Inconsistent status displays - light shows %, lock shows codes, camera shows Mbps",
isViolation: true
},
{
id: "v2",
heuristic: "h2",
name: "Match Real World",
description: "Lock status '1' and '0' instead of 'Locked'/'Unlocked'",
isViolation: true
},
{
id: "v3",
heuristic: "h6",
name: "Recognition vs Recall",
description: "Timestamps (14:32:05) don't explain what they represent",
isViolation: true
},
{
id: "v4",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Technical details like '2.4Mbps' clutter the view - most users don't need bandwidth",
isViolation: true
},
{
id: "v5",
heuristic: "iot3",
name: "Multi-Device Consistency",
description: "Each device type uses different conventions - hard to learn pattern",
isViolation: true
},
{
id: "v6",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "Grid layout allows quick overview of all devices",
isViolation: false
}
],
hint: "Look for consistency issues across different device types and technical jargon.",
expectedCount: 5
},
{
id: 8,
title: "Medical IoT Device Alarm",
context: "Patient monitoring device alarm screen",
mockup: {
type: "medical",
elements: [
"┌───────────────────────┐",
"│ Patient Monitor │",
"├───────────────────────┤",
"│ │",
"│ ⚠ ALARM ⚠ │",
"│ │",
"│ HR: 145 │",
"│ BP: 160/95 │",
"│ SpO2: 89% │",
"│ │",
"│ [Silence for 5 min] │",
"│ │",
"└───────────────────────┘"
]
},
description: "Alarm shows elevated vitals (HR=145, BP=160/95, SpO2=89%) but doesn't explain which triggered the alarm or severity. Silence button doesn't show consequences. No indication if alarm was already acknowledged by another staff member.",
violations: [
{
id: "v1",
heuristic: "h1",
name: "Visibility of System Status",
description: "Doesn't indicate WHICH vital sign triggered the alarm",
isViolation: true
},
{
id: "v2",
heuristic: "h5",
name: "Error Prevention",
description: "'Silence for 5 min' doesn't explain what happens after 5 minutes (re-alarm? disabled?)",
isViolation: true
},
{
id: "v3",
heuristic: "wcag1",
name: "Color Contrast",
description: "ALARM text same color as other info - should be high contrast red",
isViolation: true
},
{
id: "v4",
heuristic: "iot3",
name: "Multi-Device Consistency",
description: "No indication if other devices/stations also show this alarm (coordination)",
isViolation: true
},
{
id: "v5",
heuristic: "h9",
name: "Error Recovery",
description: "No guidance on appropriate response to elevated vitals",
isViolation: true
},
{
id: "v6",
heuristic: "h6",
name: "Recognition vs Recall",
description: "Vital signs labeled with standard medical abbreviations (HR, BP, SpO2)",
isViolation: false
}
],
hint: "Consider safety and critical information - what does medical staff need to know immediately?",
expectedCount: 5
}
],
expert: [
{
id: 9,
title: "Smart City Parking App",
context: "Mobile app for finding and paying for parking spots",
mockup: {
type: "mobile-map",
elements: [
"┌──────────────────────┐",
"│ ParkSmart [≡] │",
"├──────────────────────┤",
"│ │",
"│ [ Map View ] │",
"│ │",
"│ 🅿 🅿 🅿 │",
"│ 🅿 🅿 🅿 │",
"│ 🅿 🅿 │",
"│ │",
"│ Rate: $3.50/hr │",
"│ [Reserve Spot] │",
"│ │",
"└──────────────────────┘"
]
},
description: "Map shows parking spots with identical P icons - no way to tell if spot is available, occupied, reserved, or how far away. Price shows $3.50/hr but doesn't indicate if rates change (peak/off-peak). Reserve button doesn't show if reservation is guaranteed or tentative. No offline mode indication.",
violations: [
{
id: "v1",
heuristic: "h1",
name: "Visibility of System Status",
description: "All parking spots look identical - can't distinguish available vs occupied",
isViolation: true
},
{
id: "v2",
heuristic: "h6",
name: "Recognition vs Recall",
description: "No distance/time to spots - user must estimate from map",
isViolation: true
},
{
id: "v3",
heuristic: "h5",
name: "Error Prevention",
description: "Reserve button doesn't explain if spot is guaranteed or first-come-first-served",
isViolation: true
},
{
id: "v4",
heuristic: "iot2",
name: "Connectivity Status",
description: "No indication of real-time vs cached data (what if offline?)",
isViolation: true
},
{
id: "v5",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "No filters for price range, distance, or spot features (EV charging, covered, etc.)",
isViolation: true
},
{
id: "v6",
heuristic: "iot1",
name: "Feedback Latency",
description: "No indication of how often spot availability updates (5 sec? 5 min?)",
isViolation: true
},
{
id: "v7",
heuristic: "h2",
name: "Match Real World",
description: "Map metaphor matches mental model of finding parking",
isViolation: false
}
],
hint: "Think about real-time data freshness and what critical info is missing for decision-making.",
expectedCount: 6
},
{
id: 10,
title: "IoT Agricultural Sensor Dashboard",
context: "Farm management system showing sensor data from 50+ field sensors",
mockup: {
type: "ag-dashboard",
elements: [
"┌──────────────────────────────────────────────┐",
"│ Farm Monitor Dashboard [Export CSV] │",
"├──────────────────────────────────────────────┤",
"│ │",
"│ SENSOR_001: SM=42% T=18C B=3.2V TS=0415 │",
"│ SENSOR_003: SM=38% T=19C B=3.1V TS=0416 │",
"│ SENSOR_007: SM=ERR T=17C B=2.9V TS=0320 │",
"│ SENSOR_012: SM=45% T=18C B=3.3V TS=0417 │",
"│ SENSOR_018: SM=40% T=19C B=2.8V TS=0410 │",
"│ ... (45 more sensors) │",
"│ │",
"│ [Refresh] [Configure] [Alerts] │",
"│ │",
"└──────────────────────────────────────────────┘"
]
},
description: "Shows 50+ sensors in flat list with IDs (SENSOR_001), abbreviations (SM=soil moisture, T=temp, B=battery, TS=timestamp). SENSOR_007 shows SM=ERR but no alert highlighting. Low batteries not flagged. Timestamps like '0415' require interpretation. No map view of sensor locations or grouping by field zones.",
violations: [
{
id: "v1",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Overwhelming list of 50+ sensors with no grouping, filtering, or search",
isViolation: true
},
{
id: "v2",
heuristic: "h6",
name: "Recognition vs Recall",
description: "SENSOR_001, SENSOR_007 require memorizing which sensor is where in field",
isViolation: true
},
{
id: "v3",
heuristic: "wcag1",
name: "Color Contrast",
description: "Error (SM=ERR) and low battery (2.8V) not visually highlighted - same text color",
isViolation: true
},
{
id: "v4",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "No shortcuts - expert users must scroll through all 50 to find problems",
isViolation: true
},
{
id: "v5",
heuristic: "h1",
name: "Visibility of System Status",
description: "Timestamps (0415) don't show if data is fresh or stale - could be 5 min or 5 hours ago",
isViolation: true
},
{
id: "v6",
heuristic: "iot2",
name: "Connectivity Status",
description: "No indication which sensors are online vs offline (SENSOR_007 might be disconnected)",
isViolation: true
},
{
id: "v7",
heuristic: "h9",
name: "Error Recovery",
description: "SM=ERR provides no guidance on how to diagnose or fix sensor issue",
isViolation: true
},
{
id: "v8",
heuristic: "h4",
name: "Consistency & Standards",
description: "Standard abbreviations like SM, T are industry conventions",
isViolation: false
}
],
hint: "Focus on information overload and how experts need to quickly identify problems across many sensors.",
expectedCount: 7
},
{
id: 11,
title: "Smart Building HVAC Control",
context: "Building management system controlling HVAC for 10-floor office building",
mockup: {
type: "hvac-bms",
elements: [
"┌────────────────────────────────────────────────┐",
"│ Building HVAC System [Admin: User1] │",
"├────────────────────────────────────────────────┤",
"│ Zone Control: │",
"│ FL1-N: 21.5C (SP:22C) │ FL1-S: 22.8C (SP:22C)│",
"│ FL2-N: 20.1C (SP:22C) │ FL2-S: 23.5C (SP:22C)│",
"│ ... │",
"│ │",
"│ Global Override: [Enable] │",
"│ Schedule: Active (M-F 7:00-19:00) │",
"│ Energy Mode: Efficiency | Comfort | Balanced │",
"│ │",
"│ [Apply Changes] [Revert] [Schedule Editor] │",
"│ │",
"│ Last API call: 127ms ago | Queue: 14 pending │",
"└────────────────────────────────────────────────┘"
]
},
description: "Shows zones with codes (FL1-N = Floor 1 North), temps (21.5C) vs setpoints (SP:22C). Global Override button doesn't explain consequences. Energy Mode selection affects all zones but no preview of tradeoffs. 'Apply Changes' doesn't show which zones will be affected. Technical debug info (API call time, queue) distracts from core function. No alerts for zones significantly off setpoint (FL2-N at 20.1C when SP is 22C).",
violations: [
{
id: "v1",
heuristic: "h5",
name: "Error Prevention",
description: "Global Override doesn't explain it overrides schedules - could cause unexpected behavior",
isViolation: true
},
{
id: "v2",
heuristic: "h1",
name: "Visibility of System Status",
description: "FL2-N is 1.9C below setpoint but no alert - problem not visually obvious",
isViolation: true
},
{
id: "v3",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "Technical info (API call 127ms, Queue: 14) clutters interface - not needed by operators",
isViolation: true
},
{
id: "v4",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "No quick actions like 'increase all zones by 1C' - must edit individually",
isViolation: true
},
{
id: "v5",
heuristic: "h5",
name: "Error Prevention",
description: "Energy Mode selection shows no preview of impact (cost, temp range, etc.)",
isViolation: true
},
{
id: "v6",
heuristic: "h3",
name: "User Control & Freedom",
description: "Apply Changes doesn't preview which zones affected - can't review before committing",
isViolation: true
},
{
id: "v7",
heuristic: "wcag1",
name: "Color Contrast",
description: "No visual distinction between zones on-target vs off-target",
isViolation: true
},
{
id: "v8",
heuristic: "h3",
name: "User Control & Freedom",
description: "Revert button provides emergency exit if changes go wrong",
isViolation: false
}
],
hint: "Consider what happens when operators make changes - can they predict outcomes and recover from mistakes?",
expectedCount: 7
},
{
id: 12,
title: "Connected Vehicle Fleet Management",
context: "Dashboard for managing 100+ delivery vehicles with real-time tracking",
mockup: {
type: "fleet",
elements: [
"┌──────────────────────────────────────────────────┐",
"│ Fleet Command Center Vehicles: 127 │",
"├──────────────────────────────────────────────────┤",
"│ [Map] [List] [Analytics] [🔍 Search] │",
"│ │",
"│ VH-0042: EN_ROUTE │ 89 km/h │ ETA 14:32 │",
"│ VH-0103: EN_ROUTE │ 72 km/h │ ETA 15:10 │",
"│ VH-0087: STOPPED │ 0 km/h │ T=350s │",
"│ VH-0234: EN_ROUTE │ 95 km/h │ ETA 14:45 │",
"│ VH-0156: MAINT │ -- │ -- │",
"│ ... (122 more) │",
"│ │",
"│ [Send Message] [Reroute] [Emergency Alert] │",
"│ │",
"│ Conn: 124 OK │ 2 WEAK │ 1 LOST │ Sync: 2.3s │",
"└──────────────────────────────────────────────────┘"
]
},
description: "Fleet of 127 vehicles shown in list with IDs (VH-0042). VH-0087 stopped for 350 seconds but no alert highlighting. VH-0156 in MAINT mode with no details. Message/Reroute buttons don't indicate which vehicles are selected (single? multiple?). Connection status at bottom (124 OK, 2 WEAK, 1 LOST) but not tied to specific vehicles. Emergency Alert button same size/prominence as regular controls.",
violations: [
{
id: "v1",
heuristic: "wcag1",
name: "Color Contrast",
description: "Vehicle stopped 350s (potential issue) not highlighted - same color as moving vehicles",
isViolation: true
},
{
id: "v2",
heuristic: "h1",
name: "Visibility of System Status",
description: "Connection status (2 WEAK, 1 LOST) not mapped to vehicle IDs - which vehicles have issues?",
isViolation: true
},
{
id: "v3",
heuristic: "h3",
name: "User Control & Freedom",
description: "Message/Reroute buttons don't show which vehicles are selected (no checkboxes visible)",
isViolation: true
},
{
id: "v4",
heuristic: "h5",
name: "Error Prevention",
description: "Emergency Alert not visually distinct - critical action should be protected/prominent",
isViolation: true
},
{
id: "v5",
heuristic: "h8",
name: "Aesthetic & Minimalist Design",
description: "127 vehicles in flat list with no prioritization or filtering (show only issues/delays)",
isViolation: true
},
{
id: "v6",
heuristic: "h6",
name: "Recognition vs Recall",
description: "VH-0042, VH-0087 require memorizing which vehicle/driver - should show driver names",
isViolation: true
},
{
id: "v7",
heuristic: "iot1",
name: "Feedback Latency",
description: "Sync: 2.3s shown but unclear if this is good/bad/concerning latency",
isViolation: true
},
{
id: "v8",
heuristic: "h7",
name: "Flexibility & Efficiency",
description: "Search box allows quick lookup of specific vehicles",
isViolation: false
}
],
hint: "Think about managing large-scale IoT deployments - how do operators quickly identify problems and take action?",
expectedCount: 7
}
]
})
// ----------------------------------------------------------------------------
// SECTION 2: GAME STATE MANAGEMENT
// ----------------------------------------------------------------------------
import { html } from 'npm:htl';
mutable gameState = ({
difficulty: "beginner",
currentScenarioIndex: 0,
score: 0,
correctCount: 0,
missedCount: 0,
falsePositiveCount: 0,
scenariosCompleted: 0,
isGameComplete: false,
showFeedback: false,
selectedViolations: new Set(),
scenarioResults: []
})
// ----------------------------------------------------------------------------
// SECTION 3: HELPER FUNCTIONS
// ----------------------------------------------------------------------------
function getCurrentScenarios() {
return scenarios[gameState.difficulty];
}
function getCurrentScenario() {
const currentScenarios = getCurrentScenarios();
return currentScenarios[gameState.currentScenarioIndex];
}
function resetGame(difficulty = "beginner") {
mutable gameState = {
difficulty,
currentScenarioIndex: 0,
score: 0,
correctCount: 0,
missedCount: 0,
falsePositiveCount: 0,
scenariosCompleted: 0,
isGameComplete: false,
showFeedback: false,
selectedViolations: new Set(),
scenarioResults: []
};
}
function toggleViolation(violationId) {
if (gameState.showFeedback) return; // Can't change after submission
const newSelected = new Set(gameState.selectedViolations);
if (newSelected.has(violationId)) {
newSelected.delete(violationId);
} else {
newSelected.add(violationId);
}
mutable gameState = { ...gameState, selectedViolations: newSelected };
}
function submitAnswer() {
const scenario = getCurrentScenario();
const actualViolations = scenario.violations.filter(v => v.isViolation);
const actualViolationIds = new Set(actualViolations.map(v => v.id));
let correct = 0;
let missed = 0;
let falsePositives = 0;
// Check each violation
scenario.violations.forEach(v => {
const selected = gameState.selectedViolations.has(v.id);
const isActualViolation = v.isViolation;
if (selected && isActualViolation) {
correct++;
} else if (!selected && isActualViolation) {
missed++;
} else if (selected && !isActualViolation) {
falsePositives++;
}
});
// Calculate score for this scenario
const totalActual = actualViolations.length;
const scenarioScore = Math.max(0, Math.round(
(correct * 100 / totalActual) - (falsePositives * 10)
));
mutable gameState = {
...gameState,
showFeedback: true,
correctCount: gameState.correctCount + correct,
missedCount: gameState.missedCount + missed,
falsePositiveCount: gameState.falsePositiveCount + falsePositives,
score: gameState.score + scenarioScore,
scenarioResults: [...gameState.scenarioResults, {
scenarioId: scenario.id,
correct,
missed,
falsePositives,
score: scenarioScore
}]
};
}
function nextScenario() {
const currentScenarios = getCurrentScenarios();
if (gameState.currentScenarioIndex < currentScenarios.length - 1) {
mutable gameState = {
...gameState,
currentScenarioIndex: gameState.currentScenarioIndex + 1,
scenariosCompleted: gameState.scenariosCompleted + 1,
showFeedback: false,
selectedViolations: new Set()
};
} else {
mutable gameState = {
...gameState,
isGameComplete: true,
scenariosCompleted: gameState.scenariosCompleted + 1
};
}
}
function getFeedbackClass() {
const scenario = getCurrentScenario();
const result = gameState.scenarioResults[gameState.scenarioResults.length - 1];
if (!result) return "needs-work";
if (result.correct === scenario.expectedCount && result.falsePositives === 0) {
return "perfect";
} else if (result.correct >= scenario.expectedCount * 0.7) {
return "good";
} else {
return "needs-work";
}
}
function getPerformanceGrade() {
const maxPossible = gameState.scenariosCompleted * 100;
const percentage = (gameState.score / maxPossible) * 100;
if (percentage >= 90) return { grade: "A+", message: "Exceptional UX evaluation skills!", color: ieeeColors.green };
if (percentage >= 80) return { grade: "A", message: "Excellent understanding of UX heuristics!", color: ieeeColors.teal };
if (percentage >= 70) return { grade: "B", message: "Good grasp of usability principles!", color: ieeeColors.blue };
if (percentage >= 60) return { grade: "C", message: "Decent understanding, room for improvement.", color: ieeeColors.orange };
return { grade: "D", message: "Keep practicing UX evaluation!", color: ieeeColors.red };
}
// ----------------------------------------------------------------------------
// SECTION 4: RENDER GAME UI
// ----------------------------------------------------------------------------
viewof game = {
const container = html`<div class="ux-game-container"></div>`;
function render() {
if (gameState.isGameComplete) {
renderGameComplete();
} else {
renderGamePlay();
}
}
function renderGameComplete() {
const performance = getPerformanceGrade();
const maxPossible = gameState.scenariosCompleted * 100;
const percentage = Math.round((gameState.score / maxPossible) * 100);
container.replaceChildren(html`
<div class="game-header">
<h2>UX Heuristics Evaluator Game</h2>
<p>Master IoT Interface Usability Evaluation</p>
</div>
<div class="game-complete">
<div class="complete-icon">🏆</div>
<div class="complete-title">Game Complete!</div>
<div class="complete-score">Final Score: ${gameState.score} / ${maxPossible} (${percentage}%)</div>
<div class="complete-feedback" style="color: ${performance.color}">
Grade: ${performance.grade} - ${performance.message}
</div>
<div class="performance-breakdown">
<div class="performance-item">
<div class="performance-value">${gameState.correctCount}</div>
<div class="performance-label">Violations Found</div>
</div>
<div class="performance-item">
<div class="performance-value">${gameState.missedCount}</div>
<div class="performance-label">Violations Missed</div>
</div>
<div class="performance-item">
<div class="performance-value">${gameState.falsePositiveCount}</div>
<div class="performance-label">False Positives</div>
</div>
</div>
<div style="margin-top: 20px; font-size: 0.95em; color: #666; max-width: 600px; margin-left: auto; margin-right: auto;">
<p><strong>Your Performance:</strong></p>
<p>✓ You correctly identified ${gameState.correctCount} usability violations</p>
<p>✗ You missed ${gameState.missedCount} violations that were present</p>
<p>⚠ You incorrectly flagged ${gameState.falsePositiveCount} non-violations</p>
<p style="margin-top: 16px;">
${percentage >= 80
? "Excellent work! You have a strong understanding of UX heuristics and IoT-specific usability principles."
: percentage >= 60
? "Good job! Review the heuristics you missed and try again at a higher difficulty level."
: "Keep practicing! Review Nielsen's heuristics and try the beginner level again."}
</p>
</div>
<button class="restart-btn" onclick=${() => {
resetGame(gameState.difficulty);
render();
}}>
Play Again (${gameState.difficulty})
</button>
<div style="margin-top: 16px; display: flex; gap: 12px; justify-content: center;">
${gameState.difficulty !== "beginner" ? html`
<button class="restart-btn" style="background: ${ieeeColors.green}" onclick=${() => {
resetGame("beginner");
render();
}}>
Try Beginner
</button>
` : ""}
${gameState.difficulty !== "intermediate" ? html`
<button class="restart-btn" style="background: ${ieeeColors.orange}" onclick=${() => {
resetGame("intermediate");
render();
}}>
Try Intermediate
</button>
` : ""}
${gameState.difficulty !== "expert" ? html`
<button class="restart-btn" style="background: ${ieeeColors.red}" onclick=${() => {
resetGame("expert");
render();
}}>
Try Expert
</button>
` : ""}
</div>
</div>
`);
}
function renderGamePlay() {
const scenario = getCurrentScenario();
const currentScenarios = getCurrentScenarios();
const progress = ((gameState.currentScenarioIndex + 1) / currentScenarios.length) * 100;
container.replaceChildren(html`
<div class="game-header">
<h2>UX Heuristics Evaluator Game</h2>
<p>Find all usability violations in IoT interfaces</p>
</div>
<div class="game-controls">
<button class="difficulty-btn easy ${gameState.difficulty === 'beginner' ? 'active' : ''}"
onclick=${() => { resetGame("beginner"); render(); }}>
🟢 Beginner
</button>
<button class="difficulty-btn medium ${gameState.difficulty === 'intermediate' ? 'active' : ''}"
onclick=${() => { resetGame("intermediate"); render(); }}>
🟡 Intermediate
</button>
<button class="difficulty-btn hard ${gameState.difficulty === 'expert' ? 'active' : ''}"
onclick=${() => { resetGame("expert"); render(); }}>
🔴 Expert
</button>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<div class="game-stats">
<div class="stat-card">
<div class="stat-value">${gameState.currentScenarioIndex + 1}/${currentScenarios.length}</div>
<div class="stat-label">Interface</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.score}</div>
<div class="stat-label">Score</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.correctCount}</div>
<div class="stat-label">Found</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.missedCount}</div>
<div class="stat-label">Missed</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.falsePositiveCount}</div>
<div class="stat-label">False +</div>
</div>
</div>
<div class="scenario-card">
<div class="scenario-header">
<div class="scenario-title">${scenario.title}</div>
<div class="scenario-number">Scenario ${scenario.id}</div>
</div>
<div style="font-size: 0.95em; color: #666; margin-bottom: 16px;">
<strong>Context:</strong> ${scenario.context}
</div>
<div class="interface-mockup">
<div class="mockup-screen">
<pre style="margin: 0; font-size: 0.9em; line-height: 1.4;">${scenario.mockup.elements.join('\n')}</pre>
</div>
</div>
<div style="font-size: 0.95em; color: #333; line-height: 1.6; margin-bottom: 16px; padding: 12px; background: #f8f9fa; border-radius: 6px;">
<strong>Scenario:</strong> ${scenario.description}
</div>
<div class="hint-section">
<div class="hint-title">💡 Hint:</div>
${scenario.hint}
</div>
<div class="violations-section">
<div class="violations-title">
Select ALL usability violations you can find: (${scenario.expectedCount} violations present)
</div>
<div class="violations-grid">
${scenario.violations.map(violation => {
const isSelected = gameState.selectedViolations.has(violation.id);
const heuristic = heuristics.find(h => h.id === violation.heuristic);
let cssClass = "violation-checkbox";
if (gameState.showFeedback) {
if (violation.isViolation && isSelected) {
cssClass += " correct";
} else if (violation.isViolation && !isSelected) {
cssClass += " missed";
} else if (!violation.isViolation && isSelected) {
cssClass += " incorrect";
}
}
return html`
<div class="${cssClass}" onclick=${() => { toggleViolation(violation.id); render(); }}>
<input type="checkbox"
${isSelected ? "checked" : ""}
onclick=${(e) => e.stopPropagation()}
onchange=${() => { toggleViolation(violation.id); render(); }}
/>
<div class="violation-label">
<div class="violation-heuristic">
${violation.name}
${gameState.showFeedback ? (violation.isViolation ? " ✓" : " ✗") : ""}
</div>
<div class="violation-description">${violation.description}</div>
</div>
</div>
`;
})}
</div>
</div>
<div class="action-buttons">
<button class="action-btn submit"
${gameState.showFeedback ? "disabled" : ""}
onclick=${() => { submitAnswer(); render(); }}>
✓ Submit Answer
</button>
${gameState.showFeedback ? html`
<button class="action-btn next" onclick=${() => { nextScenario(); render(); }}>
Next Interface →
</button>
` : ""}
</div>
${gameState.showFeedback ? renderFeedback() : ""}
</div>
`);
}
function renderFeedback() {
const scenario = getCurrentScenario();
const result = gameState.scenarioResults[gameState.scenarioResults.length - 1];
const feedbackClass = getFeedbackClass();
let feedbackTitle = "";
let feedbackIcon = "";
if (feedbackClass === "perfect") {
feedbackTitle = "Perfect! 🎉";
feedbackIcon = "✓";
} else if (feedbackClass === "good") {
feedbackTitle = "Good Job! 👍";
feedbackIcon = "✓";
} else {
feedbackTitle = "Needs Review 📝";
feedbackIcon = "⚠";
}
return html`
<div class="feedback-panel show ${feedbackClass}">
<div class="feedback-title">
${feedbackIcon} ${feedbackTitle}
</div>
<div class="feedback-score">
Score: <strong>${result.score}/100</strong> for this interface
</div>
<div class="feedback-details">
<div class="violations-summary">
${result.correct > 0 ? html`
<div class="summary-item">
<span class="summary-icon">✅</span>
<span>You correctly identified <strong>${result.correct}</strong> violation${result.correct !== 1 ? 's' : ''}</span>
</div>
` : ""}
${result.missed > 0 ? html`
<div class="summary-item">
<span class="summary-icon">⚠️</span>
<span>You missed <strong>${result.missed}</strong> violation${result.missed !== 1 ? 's' : ''} (highlighted in yellow above)</span>
</div>
` : ""}
${result.falsePositives > 0 ? html`
<div class="summary-item">
<span class="summary-icon">❌</span>
<span>You incorrectly flagged <strong>${result.falsePositives}</strong> non-violation${result.falsePositives !== 1 ? 's' : ''} (highlighted in red above)</span>
</div>
` : ""}
${result.correct === scenario.expectedCount && result.falsePositives === 0 ? html`
<div class="summary-item">
<span class="summary-icon">🌟</span>
<span>Perfect score! You found all violations with no false positives!</span>
</div>
` : ""}
</div>
<div style="margin-top: 12px; font-size: 0.9em; color: #555;">
<strong>Review the violations above:</strong> Green = correctly identified, Yellow = missed, Red = false positive
</div>
</div>
</div>
`;
}
render();
return container;
}