feat: enhance tooltips with combined provenance and pedagogical content

Implement sophisticated tooltip system that shows both source digit context and pedagogical reasoning for different operation types.

Key improvements:
- Direct operations: Enhanced provenance titles like "Add the tens digit — 2 tens (20)"
- Complement operations: Combined provenance context + pedagogical why explanations
- Enhanced chips with source digit information for all operation types
- Fixed context integration to remove prop drilling (steps now from tutorial context)
- Preserved mathematical accuracy while improving pedagogical clarity

For five-complement (3 + 4 = 7), tooltips now show:
- Source context: "From ones digit 4 of 4"
- Pedagogical reasoning: "Adding 4 would need more lower beads than we have"
- Strategic explanation: "Use the heaven bead instead: press it and lift some lower beads"

This addresses the original problem where students couldn't understand that "20" came from the "2" in "25", while maintaining rich educational content for complement strategies.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-26 09:21:09 -05:00
parent 37b5ae8623
commit 0c7ad5e4e7
2 changed files with 91 additions and 17 deletions

View File

@@ -57,7 +57,7 @@ interface DecompositionWithReasonsProps {
termPositions: Array<{ startIndex: number; endIndex: number }>
segments?: PedagogicalSegment[]
termReasons?: TermReason[]
steps?: UnifiedStepData[]
// NOTE: steps now comes from tutorial context, not props
}
interface TermSpanProps {
@@ -102,18 +102,27 @@ interface SegmentGroupProps {
fullDecomposition: string
termPositions: Array<{ startIndex: number; endIndex: number }>
termReasons?: TermReason[]
steps: UnifiedStepData[]
children: React.ReactNode
}
function SegmentGroup({ segment, fullDecomposition, steps, children }: SegmentGroupProps) {
function SegmentGroup({ segment, fullDecomposition, children }: SegmentGroupProps) {
const [tooltipOpen, setTooltipOpen] = useState(false)
const { addActiveTerm, removeActiveTerm } = useContext(DecompositionContext)
// Get steps from tutorial context instead of props
const { unifiedSteps: steps } = useTutorialContext()
// Calculate the original term that was expanded
// digit * 10^place gives us the original value (e.g., digit=5, place=1 -> 50)
const originalValue = (segment.digit * Math.pow(10, segment.place)).toString()
// Get provenance from the first step in this segment
const firstStepIndex = segment.termIndices[0]
const firstStep = steps[firstStepIndex]
const provenance = firstStep?.provenance
const handleTooltipChange = (open: boolean) => {
setTooltipOpen(open)
// Activate/deactivate all terms in this segment
@@ -132,6 +141,7 @@ function SegmentGroup({ segment, fullDecomposition, steps, children }: SegmentGr
steps={steps}
open={tooltipOpen}
onOpenChange={handleTooltipChange}
provenance={provenance} // NEW: Pass provenance data
>
<span
className="segment-group"
@@ -150,8 +160,7 @@ export function DecompositionWithReasons({
fullDecomposition,
termPositions,
segments,
termReasons,
steps = []
termReasons
}: DecompositionWithReasonsProps) {
const [activeTerms, setActiveTerms] = useState<Set<number>>(new Set())
@@ -269,7 +278,6 @@ export function DecompositionWithReasons({
fullDecomposition={fullDecomposition}
termPositions={termPositions}
termReasons={termReasons}
steps={steps}
>
{segmentElements}
</SegmentGroup>

View File

@@ -3,7 +3,7 @@
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'
import type { UnifiedStepData, TermProvenance } from '../../utils/unifiedStepGenerator'
interface ReasonTooltipProps {
children: React.ReactNode
@@ -14,6 +14,7 @@ interface ReasonTooltipProps {
steps?: UnifiedStepData[]
open?: boolean
onOpenChange?: (open: boolean) => void
provenance?: TermProvenance // NEW: Provenance data for enhanced tooltips
}
// Fallback utility for legacy support
@@ -30,7 +31,8 @@ export function ReasonTooltip({
originalValue,
steps,
open,
onOpenChange
onOpenChange,
provenance
}: ReasonTooltipProps) {
const [showBeadDetails, setShowBeadDetails] = useState(false)
const [showMath, setShowMath] = useState(false)
@@ -42,9 +44,51 @@ export function ReasonTooltip({
return <>{children}</>
}
// Use readable format from segment
// Use readable format from segment, enhanced with provenance
const readable = segment?.readable
// Generate enhanced tooltip content using provenance
const getEnhancedTooltipContent = () => {
if (!provenance) return null
// For Direct operations, use the enhanced provenance title
if (rule === 'Direct') {
const title = `Add the ${provenance.rhsPlaceName} digit — ${provenance.rhsDigit} ${provenance.rhsPlaceName} (${provenance.rhsValue})`
const subtitle = `From addend ${provenance.rhs}`
const enhancedChips = [
{ label: 'Digit we\'re using', value: `${provenance.rhsDigit} (${provenance.rhsPlaceName})` },
...(readable?.chips.find(chip => chip.label === 'This rod shows') ? [
{ label: 'This rod shows', value: readable.chips.find(chip => chip.label === 'This rod shows')!.value }
] : []),
{ label: 'So we add here', value: `+${provenance.rhsDigit} ${provenance.rhsPlaceName}${provenance.rhsValue}` }
]
return { title, subtitle, chips: enhancedChips }
}
// For complement operations, enhance the existing readable content with provenance context
if (readable) {
// Keep the readable title but add provenance context to subtitle
const title = readable.title
const subtitle = `${readable.subtitle || ''} • From ${provenance.rhsPlaceName} digit ${provenance.rhsDigit} of ${provenance.rhs}`.trim()
// Enhance the chips by adding provenance context at the beginning
const enhancedChips = [
{ label: 'Source digit', value: `${provenance.rhsDigit} from ${provenance.rhs} (${provenance.rhsPlaceName} place)` },
...readable.chips
]
return { title, subtitle, chips: enhancedChips }
}
return null
}
const enhancedContent = getEnhancedTooltipContent()
const getRuleInfo = (rule: PedagogicalRule) => {
switch (rule) {
case 'Direct':
@@ -126,18 +170,20 @@ export function ReasonTooltip({
<div className="reason-tooltip__header">
<span className="reason-tooltip__emoji">{ruleInfo.emoji}</span>
<div className="reason-tooltip__title">
<h4 className="reason-tooltip__name">{readable?.title || ruleInfo.name}</h4>
<h4 className="reason-tooltip__name">
{enhancedContent?.title || readable?.title || ruleInfo.name}
</h4>
<p id={`${tooltipId}-description`} className="reason-tooltip__description">
{readable?.subtitle || ruleInfo.description}
{enhancedContent?.subtitle || readable?.subtitle || ruleInfo.description}
</p>
</div>
</div>
{/* Context chips using readable format */}
{readable && readable.chips.length > 0 && (
{/* Context chips using enhanced or readable format */}
{(enhancedContent?.chips || readable?.chips) && (
<div className="reason-tooltip__context">
<div className="reason-tooltip__chips">
{readable.chips.map((chip, index) => (
{(enhancedContent?.chips || readable?.chips || []).map((chip, index) => (
<span key={index} className="reason-tooltip__chip">
{chip.label}: {chip.value}
</span>
@@ -146,15 +192,35 @@ export function ReasonTooltip({
</div>
)}
{/* Why this step using readable format */}
{readable && readable.why.length > 0 && (
{/* Why this step using enhanced provenance or readable format */}
{(provenance || (readable && readable.why.length > 0)) && (
<div className="reason-tooltip__reasoning">
<h5 className="reason-tooltip__section-title">Why this step</h5>
{readable.why.map((why, index) => (
{/* Show provenance explanation for Direct rules */}
{provenance && rule === 'Direct' && (
<>
<p className="reason-tooltip__explanation-text">
We're adding the <strong>{provenance.rhsPlaceName} digit</strong> of <strong>{provenance.rhs}</strong> → <strong>{provenance.rhsDigit} {provenance.rhsPlaceName}</strong>.
</p>
{readable?.chips.find(chip => chip.label === 'This rod shows') && (
<p className="reason-tooltip__explanation-text">
• {readable.chips.find(chip => chip.label === 'This rod shows')?.label} <strong>{readable.chips.find(chip => chip.label === 'This rod shows')?.value}</strong>; adding <strong>{provenance.rhsDigit}</strong> fits, so no carry.
</p>
)}
</>
)}
{/* Show readable why explanations for complement rules (and optionally Direct if available) */}
{readable && readable.why.length > 0 && readable.why.map((why, index) => (
<p key={index} className="reason-tooltip__explanation-text">
• {why}
</p>
))}
{/* For complement rules with provenance, add additional context about source digit */}
{provenance && rule !== 'Direct' && (
<p className="reason-tooltip__explanation-text">
• This expansion processes the <strong>{provenance.rhsPlaceName} digit {provenance.rhsDigit}</strong> from the addend <strong>{provenance.rhs}</strong>.
</p>
)}
</div>
)}