feat: implement comprehensive bead diff tooltips with pedagogical decomposition

- Add PedagogicalDecompositionDisplay component with tooltip and guidance variants
- Create Radix tooltip overlay system targeting specific beads with direction arrows
- Show pedagogical decomposition with current term highlighted in both tooltip and guidance pane
- Position tooltip to left of abacus with 60px left margin to prevent clipping
- Add hover opacity effects (85% default, 100% on hover) for subtle tooltip presence
- Include lightbulb emoji and action summary in tooltip for clear guidance

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-24 08:04:09 -05:00
parent 8a9065dd9b
commit 2e3223da90
3 changed files with 258 additions and 39 deletions

View File

@@ -0,0 +1,84 @@
'use client'
import React from 'react'
interface DecompositionData {
before: string
highlighted: string
after: string
}
interface PedagogicalDecompositionDisplayProps {
variant: 'tooltip' | 'guidance'
showLabel?: boolean
decomposition: DecompositionData | null
}
export function PedagogicalDecompositionDisplay({
variant = 'guidance',
showLabel = false,
decomposition
}: PedagogicalDecompositionDisplayProps) {
if (!decomposition || (!decomposition.before && !decomposition.highlighted && !decomposition.after)) {
return null
}
if (variant === 'tooltip') {
return (
<>
{showLabel && (
<div style={{ fontSize: '10px', opacity: 0.7, marginBottom: '2px', textAlign: 'center' }}>
Working on:
</div>
)}
<div style={{
fontSize: '10px',
marginBottom: '6px',
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden'
}}>
<span style={{ opacity: 0.4, color: 'white' }}>
{decomposition.before}
</span>
<span style={{
fontSize: '13px',
fontWeight: 'bold',
color: '#fbbf24',
backgroundColor: 'rgba(251, 191, 36, 0.2)',
padding: '2px 6px',
borderRadius: '4px',
border: '1px solid rgba(251, 191, 36, 0.4)'
}}>
{decomposition.highlighted}
</span>
<span style={{ opacity: 0.4, color: 'white' }}>
{decomposition.after}
</span>
</div>
</>
)
}
// Guidance variant (existing styles)
return (
<span>
{decomposition.before}
<span style={{
background: 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(147,51,234,0.1) 100%)',
color: '#1e3a8a',
padding: '4px 8px',
borderRadius: '6px',
border: '1px solid rgba(59,130,246,0.3)',
fontWeight: '600',
boxShadow: '0 1px 3px rgba(59,130,246,0.1), inset 0 1px 0 rgba(255,255,255,0.6)',
backdropFilter: 'blur(2px)',
display: 'inline-block'
}}>
{decomposition.highlighted}
</span>
{decomposition.after}
</span>
)
}

View File

@@ -11,6 +11,7 @@ import { generateAbacusInstructions } from '../../utils/abacusInstructionGenerat
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
import { generateUnifiedInstructionSequence } from '../../utils/unifiedStepGenerator'
import { TutorialProvider } from './TutorialContext'
import { PedagogicalDecompositionDisplay } from './PedagogicalDecompositionDisplay'
// Helper function to find the topmost bead with arrows
function findTopmostBeadWithArrows(stepBeadHighlights: StepBeadHighlight[] | undefined): StepBeadHighlight | null {
@@ -331,6 +332,102 @@ function TutorialPlayerContent({
// Get current step summary for real-time user feedback
const currentStepSummary = getCurrentStepSummary()
// Helper function to highlight the current mathematical term in the full decomposition
const renderHighlightedDecomposition = useCallback(() => {
if (!fullDecomposition || expectedSteps.length === 0) return null
const currentStep = expectedSteps[currentMultiStep]
if (!currentStep?.termPosition) return null
const { startIndex, endIndex } = currentStep.termPosition
const before = fullDecomposition.substring(0, startIndex)
const highlighted = fullDecomposition.substring(startIndex, endIndex)
const after = fullDecomposition.substring(endIndex)
return { before, highlighted, after }
}, [fullDecomposition, expectedSteps, currentMultiStep])
// Create overlay for tooltip positioned precisely at topmost bead using AbacusReact's overlay system
const tooltipOverlay = useMemo(() => {
if (!currentStepSummary || !currentStepBeads?.length) {
return null
}
// Find the topmost bead with arrows
const topmostBead = findTopmostBeadWithArrows(currentStepBeads)
if (!topmostBead) {
return null
}
// Create an overlay that targets the specific bead and positions tooltip outside abacus
const overlay: AbacusOverlay = {
id: 'bead-tooltip',
type: 'tooltip',
target: {
type: 'bead',
columnIndex: 4 - topmostBead.placeValue, // Convert placeValue to columnIndex (5 columns: 0-4)
beadType: topmostBead.beadType,
beadPosition: topmostBead.position
},
content: (
<Tooltip.Provider>
<Tooltip.Root open={true}>
<Tooltip.Trigger asChild>
<div style={{ width: '1px', height: '1px', opacity: 0 }} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side="left"
align="center"
sideOffset={20}
style={{
background: '#1e40af',
color: 'white',
padding: '12px 16px',
borderRadius: '8px',
fontSize: '14px',
fontWeight: '700',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
whiteSpace: 'normal',
maxWidth: '200px',
minWidth: '150px',
wordBreak: 'break-word',
zIndex: 1000,
opacity: 0.85,
transition: 'opacity 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1'
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.85'
}}
>
<div style={{ fontSize: '12px', opacity: 0.9 }}>
<PedagogicalDecompositionDisplay
variant="tooltip"
showLabel={true}
decomposition={renderHighlightedDecomposition()}
/>
<span style={{ fontSize: '18px' }}>💡</span> {currentStepSummary}
</div>
<Tooltip.Arrow
style={{
fill: '#1e40af'
}}
/>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
),
offset: { x: 0, y: 0 },
visible: true
}
return overlay
}, [currentStepSummary, currentStepBeads])
// Timer for smart help detection
useEffect(() => {
setShowHelpForCurrentStep(false) // Reset help when step changes
@@ -342,43 +439,6 @@ function TutorialPlayerContent({
return () => clearTimeout(timer)
}, [currentMultiStep, multiStepStartTime]) // Reset when step changes or timer resets
// Helper function to highlight the current mathematical term in the full decomposition
const renderHighlightedDecomposition = useCallback(() => {
if (!fullDecomposition || expectedSteps.length === 0) return null
const currentStep = expectedSteps[currentMultiStep]
if (!currentStep?.mathematicalTerm || !currentStep?.termPosition) {
return <span>{fullDecomposition}</span>
}
// Use the precise position information from the unified generator
const { startIndex, endIndex } = currentStep.termPosition
const beforeTerm = fullDecomposition.substring(0, startIndex)
const term = fullDecomposition.substring(startIndex, endIndex)
const afterTerm = fullDecomposition.substring(endIndex)
return (
<span>
{beforeTerm}
<span className={css({
background: 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(147,51,234,0.1) 100%)',
color: 'blue.900',
px: 2,
py: 1,
borderRadius: 'md',
border: '1px solid rgba(59,130,246,0.3)',
fontWeight: '600',
boxShadow: '0 1px 3px rgba(59,130,246,0.1), inset 0 1px 0 rgba(255,255,255,0.6)',
backdropFilter: 'blur(2px)',
display: 'inline-block'
})}>
{term}
</span>
{afterTerm}
</span>
)
}, [fullDecomposition, expectedSteps, currentMultiStep])
// Event logging - now just notifies parent, state is managed by reducer
const notifyEvent = useCallback((event: TutorialEvent) => {
onEvent?.(event)
@@ -965,7 +1025,10 @@ function TutorialPlayerContent({
letterSpacing: 'tight',
lineHeight: '1.5'
})}>
{renderHighlightedDecomposition()}
<PedagogicalDecompositionDisplay
variant="guidance"
decomposition={renderHighlightedDecomposition()}
/>
</p>
</div>
)}
@@ -1044,7 +1107,8 @@ function TutorialPlayerContent({
borderColor: 'gray.200',
borderRadius: 'lg',
p: 6,
shadow: 'lg'
shadow: 'lg',
ml: '60px' // Left margin to accommodate tooltip
})}>
<AbacusReact
value={currentValue}
@@ -1058,6 +1122,7 @@ function TutorialPlayerContent({
currentStep={currentMultiStep}
showDirectionIndicators={true}
customStyles={customStyles}
overlays={tooltipOverlay ? [tooltipOverlay] : []}
onValueChange={handleValueChange}
callbacks={{
onBeadClick: handleBeadClick,

View File

@@ -0,0 +1,70 @@
{
"permissions": {
"allow": [
"Bash(typst:*)",
"Read(//Users/antialias/projects/soroban-abacus-flashcards/packages/**)",
"Bash(npm install:*)",
"Bash(pnpm add:*)",
"Bash(npm run gallery:node:*)",
"Bash(node:*)",
"Bash(pnpm info:*)",
"Bash(pnpm remove:*)",
"Bash(pnpm search:*)",
"Bash(pnpm run:*)",
"Bash(npm run gallery:*)",
"Bash(cp:*)",
"Bash(/dev/null)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Read(//Users/antialias/projects/soroban-abacus-flashcards/**)",
"Bash(pnpm install:*)",
"Bash(pkill:*)",
"Bash(timeout 30s pnpm run build-storybook)",
"Bash(timeout 60s pnpm run build-storybook)",
"Bash(curl:*)",
"WebFetch(domain:localhost)",
"Bash(pnpm list:*)",
"Bash(git reset:*)",
"Bash(git checkout:*)",
"Bash(cat:*)",
"Bash(git log:*)",
"Bash(python:*)",
"Bash(pnpm test:*)",
"Bash(timeout:*)",
"Bash(pnpm build:*)",
"Bash(npx playwright:*)",
"Bash(npx storybook@latest init:*)",
"Bash(pnpm update:*)",
"Bash(npm run build:*)",
"Bash(npm run dev:*)",
"Bash(npm test:*)",
"Bash(npx tsc:*)",
"WebSearch",
"WebFetch(domain:www.npmjs.com)",
"Bash(open http://localhost:3001/tutorial-editor)",
"Bash(open http://localhost:3004/tutorial-editor)",
"Bash(git push:*)",
"Bash(open http://localhost:3005/tutorial-editor)",
"Bash(find:*)",
"Bash(pnpm vitest:*)",
"Bash(open http://localhost:3006/tutorial-editor)",
"Bash(open http://localhost:3007/tutorial-editor)",
"Bash(open http://localhost:3000/tutorial-editor)",
"Bash(npx vitest:*)",
"Bash(open http://localhost:3008/tutorial-editor)",
"Bash(npm run storybook:*)",
"Bash(open http://localhost:6006/)",
"Bash(npm run test:*)",
"Bash(git rm:*)",
"Bash(git check-ignore:*)",
"Bash(killall:*)",
"Bash(npx tsx:*)",
"Bash(open http://localhost:3002/tutorial-editor)",
"Bash(xargs rm:*)",
"WebFetch(domain:www.radix-ui.com)",
"Bash(git stash:*)"
],
"deny": [],
"ask": []
}
}