feat: enhance pedagogical reasoning tooltips with comprehensive context
✨ Core improvements: - Add local context chips showing place, a, d, and s values for immediate situational awareness - Implement expandable step-by-step breakdown with clickable chevron interface - Name carry paths explicitly for ten complement operations (cascade vs simple) - Standardize rule naming: "Five Friend", "Ten Friend", "Ten Friend (cascade)" 🎯 Pedagogical enhancements: - Show concrete state values (a=4, d=6, s=4) to anchor abstract rules to current context - Provide carry path descriptions: "tens=9 ⇒ hundreds+1, tens→0" for cascade operations - Enable expandable bead movement details while maintaining compact default view - Consistent rule taxonomy across all complement types ♿ Accessibility improvements: - Full keyboard navigation: Enter/Space to toggle, Escape to close - Proper ARIA labels and descriptions for screen readers - Focus management and visual focus indicators - Semantic button roles and expanded state announcements 🎨 Polish: - Clean expandable interface with animated chevron - Context chips with monospace font for precise values - Highlighted carry path boxes for ten complements - Consistent visual hierarchy and spacing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import type { PedagogicalRule, PedagogicalSegment, TermReason } from './DecompositionWithReasons'
|
||||
import type { UnifiedStepData } from '../../utils/unifiedStepGenerator'
|
||||
@@ -16,6 +16,28 @@ interface ReasonTooltipProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function getPlaceName(place: number): string {
|
||||
const names = ['ones', 'tens', 'hundreds', 'thousands', 'ten-thousands']
|
||||
return names[place] || `10^${place}`
|
||||
}
|
||||
|
||||
function formatRuleName(rule: PedagogicalRule, place: number, hasCascade = false): string {
|
||||
const placeName = getPlaceName(place)
|
||||
switch (rule) {
|
||||
case 'Direct':
|
||||
return `Direct — ${placeName}`
|
||||
case 'FiveComplement':
|
||||
return `Five Friend — ${placeName}`
|
||||
case 'TenComplement':
|
||||
return hasCascade ? `Ten Friend (cascade) — ${placeName}` : `Ten Friend — ${placeName}`
|
||||
case 'Cascade':
|
||||
return `Chain Reaction — ${placeName}`
|
||||
default:
|
||||
return `Strategy — ${placeName}`
|
||||
}
|
||||
}
|
||||
|
||||
export function ReasonTooltip({
|
||||
children,
|
||||
termIndex,
|
||||
@@ -26,6 +48,7 @@ export function ReasonTooltip({
|
||||
open,
|
||||
onOpenChange
|
||||
}: ReasonTooltipProps) {
|
||||
const [showBeadDetails, setShowBeadDetails] = useState(false)
|
||||
const rule = reason?.rule ?? segment?.plan[0]?.rule
|
||||
const shortReason = reason?.shortReason
|
||||
const bullets = reason?.bullets
|
||||
@@ -34,6 +57,17 @@ export function ReasonTooltip({
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// Calculate context values
|
||||
const place = segment?.place ?? 0
|
||||
const currentDigit = segment?.a ?? 0 // Current digit at this place
|
||||
const addingDigit = segment?.digit ?? 0 // Digit being added
|
||||
const sum = currentDigit + addingDigit
|
||||
const complement = rule === 'FiveComplement' ? (5 - currentDigit) :
|
||||
rule === 'TenComplement' ? (10 - addingDigit) : 0
|
||||
|
||||
// Detect cascade
|
||||
const hasCascade = segment?.plan?.some(p => p.rule === 'Cascade') ?? false
|
||||
|
||||
const getRuleInfo = (rule: PedagogicalRule) => {
|
||||
switch (rule) {
|
||||
case 'Direct':
|
||||
@@ -77,29 +111,66 @@ export function ReasonTooltip({
|
||||
const ruleInfo = getRuleInfo(rule)
|
||||
const contentClasses = `reason-tooltip reason-tooltip--${ruleInfo.color}`
|
||||
|
||||
const tooltipId = `tooltip-${termIndex}`
|
||||
const ruleTitle = formatRuleName(rule, place, hasCascade)
|
||||
|
||||
return (
|
||||
<Tooltip.Root open={open} onOpenChange={onOpenChange} delayDuration={300}>
|
||||
<Tooltip.Trigger asChild>
|
||||
{children}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-labelledby={tooltipId}
|
||||
aria-describedby={`${tooltipId}-description`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onOpenChange?.(!open)
|
||||
}
|
||||
if (e.key === 'Escape' && open) {
|
||||
onOpenChange?.(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
id={tooltipId}
|
||||
className={contentClasses}
|
||||
sideOffset={8}
|
||||
side="top"
|
||||
align="center"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={() => onOpenChange?.(false)}
|
||||
>
|
||||
<div className="reason-tooltip__content">
|
||||
<div className="reason-tooltip__header">
|
||||
<span className="reason-tooltip__emoji">{ruleInfo.emoji}</span>
|
||||
<div className="reason-tooltip__title">
|
||||
<h4 className="reason-tooltip__name">{ruleInfo.name}</h4>
|
||||
<p className="reason-tooltip__description">{ruleInfo.description}</p>
|
||||
<h4 className="reason-tooltip__name">{ruleTitle}</h4>
|
||||
<p id={`${tooltipId}-description`} className="reason-tooltip__description">{ruleInfo.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Context chips */}
|
||||
{segment && (
|
||||
<div className="reason-tooltip__context">
|
||||
<div className="reason-tooltip__chips">
|
||||
<span className="reason-tooltip__chip">Place: {getPlaceName(place)}</span>
|
||||
<span className="reason-tooltip__chip">a = {currentDigit}</span>
|
||||
<span className="reason-tooltip__chip">d = {addingDigit}</span>
|
||||
{complement > 0 && (
|
||||
<span className="reason-tooltip__chip">
|
||||
s = {rule === 'FiveComplement' ? 5 : 10} − {rule === 'FiveComplement' ? currentDigit : addingDigit} = {complement}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shortReason && (
|
||||
<div className="reason-tooltip__explanation">
|
||||
<p>{shortReason}</p>
|
||||
@@ -122,6 +193,23 @@ export function ReasonTooltip({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show carry path for ten complements */}
|
||||
{rule === 'TenComplement' && (
|
||||
<div className="reason-tooltip__carry-path">
|
||||
<p className="reason-tooltip__carry-description">
|
||||
{hasCascade ? (
|
||||
<>
|
||||
<strong>Carry path:</strong> {getPlaceName(place + 1)} = 9 ⇒ find nearest non-9, then clear intermediate 9s
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong>Carry path:</strong> +1 to {getPlaceName(place + 1)}, -{addingDigit} from {getPlaceName(place)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bullets && bullets.length > 0 && (
|
||||
<div className="reason-tooltip__details">
|
||||
<ul>
|
||||
@@ -135,23 +223,45 @@ export function ReasonTooltip({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show step-by-step breakdown for multi-step segments */}
|
||||
{/* Show expandable step-by-step breakdown for multi-step segments */}
|
||||
{segment && steps && segment.stepIndices && segment.stepIndices.length > 1 && (
|
||||
<div className="reason-tooltip__steps">
|
||||
<h5 className="reason-tooltip__section-title">Step-by-step breakdown:</h5>
|
||||
<ol className="reason-tooltip__step-list">
|
||||
{segment.stepIndices.map((stepIndex, idx) => {
|
||||
const step = steps[stepIndex]
|
||||
if (!step) return null
|
||||
<button
|
||||
className="reason-tooltip__expand-button"
|
||||
onClick={() => setShowBeadDetails(!showBeadDetails)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
setShowBeadDetails(!showBeadDetails)
|
||||
}
|
||||
}}
|
||||
aria-expanded={showBeadDetails}
|
||||
aria-controls={`${tooltipId}-steps`}
|
||||
type="button"
|
||||
>
|
||||
<span className="reason-tooltip__section-title">
|
||||
Step-by-step breakdown
|
||||
<span className="reason-tooltip__chevron" style={{ transform: showBeadDetails ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
▼
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
return (
|
||||
<li key={stepIndex} className="reason-tooltip__step">
|
||||
<code className="reason-tooltip__step-term">{step.mathematicalTerm}</code>
|
||||
<span className="reason-tooltip__step-instruction">{step.englishInstruction}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
{showBeadDetails && (
|
||||
<ol id={`${tooltipId}-steps`} className="reason-tooltip__step-list">
|
||||
{segment.stepIndices.map((stepIndex, idx) => {
|
||||
const step = steps[stepIndex]
|
||||
if (!step) return null
|
||||
|
||||
return (
|
||||
<li key={stepIndex} className="reason-tooltip__step">
|
||||
<code className="reason-tooltip__step-term">{step.mathematicalTerm}</code>
|
||||
<span className="reason-tooltip__step-instruction">{step.englishInstruction}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -198,13 +198,13 @@
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0 0 4px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 4px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.reason-tooltip__reasoning {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.reason-tooltip__decision {
|
||||
@@ -219,20 +219,22 @@
|
||||
}
|
||||
|
||||
.reason-tooltip__steps {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.reason-tooltip__step-list {
|
||||
margin: 0;
|
||||
padding: 0 0 0 16px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding: 0 0 0 12px;
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.reason-tooltip__step {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.reason-tooltip__step:last-child {
|
||||
@@ -265,6 +267,88 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Context chips */
|
||||
.reason-tooltip__context {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.reason-tooltip__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.reason-tooltip__chip {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* Expandable step section */
|
||||
.reason-tooltip__expand-button {
|
||||
background: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.reason-tooltip__expand-button:hover .reason-tooltip__section-title {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.reason-tooltip__expand-button:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.reason-tooltip__expand-button:focus .reason-tooltip__section-title {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.reason-tooltip__section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0 0 4px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.reason-tooltip__chevron {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s ease;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Carry path section */
|
||||
.reason-tooltip__carry-path {
|
||||
margin: 8px 0;
|
||||
padding: 6px 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.reason-tooltip__carry-description {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.reason-tooltip__code {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
Reference in New Issue
Block a user