feat(practice): improve help UX with coach hints and simplified UI
- Add coach hint generator using same hints as tutorial (segment.readable.summary) - Hide abacus numerals in help mode for cleaner display - Restructure layout: abacus centered, coaching panel on right side - Exit help mode completely when student finishes adding term to abacus - Remove checkmarks from term indicators, keep only arrow for current term - Clean up unused props (confirmedTermCount, detectedPrefixIndex, countdownElement) - Add HelpCountdown component (pie chart timer, not currently used) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9a4ab8296e
commit
19169ad9fe
|
|
@ -20,6 +20,8 @@ import {
|
|||
generateSingleProblem,
|
||||
} from '@/utils/problemGenerator'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { DecompositionDisplay, DecompositionProvider } from '../decomposition'
|
||||
import { generateCoachHint } from './coachHintGenerator'
|
||||
import { useHasPhysicalKeyboard } from './hooks/useDeviceDetection'
|
||||
import { NumericKeypad } from './NumericKeypad'
|
||||
import { PracticeHelpOverlay } from './PracticeHelpOverlay'
|
||||
|
|
@ -529,24 +531,16 @@ export function ActiveSession({
|
|||
}, [helpActions])
|
||||
|
||||
// Handle when student reaches the target value on the help abacus
|
||||
// This exits help mode completely and resets the problem to normal state
|
||||
const handleTargetReached = useCallback(() => {
|
||||
if (helpTermIndex === null || !currentProblem) return
|
||||
|
||||
// Current term is now confirmed
|
||||
const newConfirmedCount = helpTermIndex + 1
|
||||
setConfirmedTermCount(newConfirmedCount)
|
||||
|
||||
// Brief delay so user sees the success feedback
|
||||
// Brief delay so user sees the success feedback, then exit help mode completely
|
||||
setTimeout(() => {
|
||||
// If there's another term after this one, move to it
|
||||
if (newConfirmedCount < currentProblem.problem.terms.length) {
|
||||
setHelpTermIndex(newConfirmedCount)
|
||||
setUserAnswer('')
|
||||
} else {
|
||||
// This was the last term - hide help and let them type the final answer
|
||||
// Reset all help-related state - problem returns to as if they never entered a prefix
|
||||
setHelpTermIndex(null)
|
||||
setConfirmedTermCount(0)
|
||||
setUserAnswer('')
|
||||
}
|
||||
}, 800) // 800ms delay to show "Perfect!" feedback
|
||||
}, [helpTermIndex, currentProblem])
|
||||
|
||||
|
|
@ -1022,7 +1016,18 @@ export function ActiveSession({
|
|||
{currentSlot?.purpose}
|
||||
</div>
|
||||
|
||||
{/* Problem display - vertical or linear based on part type */}
|
||||
{/* Problem display - horizontal layout with help panel on right when in help mode */}
|
||||
<div
|
||||
data-element="problem-with-help"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
gap: '1.5rem',
|
||||
width: '100%',
|
||||
})}
|
||||
>
|
||||
{/* Center: Problem display */}
|
||||
{currentPart.format === 'vertical' ? (
|
||||
<VerticalProblem
|
||||
terms={currentProblem.problem.terms}
|
||||
|
|
@ -1031,54 +1036,11 @@ export function ActiveSession({
|
|||
isCompleted={feedback !== 'none'}
|
||||
correctAnswer={currentProblem.problem.answer}
|
||||
size="large"
|
||||
confirmedTermCount={confirmedTermCount}
|
||||
currentHelpTermIndex={helpTermIndex ?? undefined}
|
||||
detectedPrefixIndex={
|
||||
matchedPrefixIndex >= 0 && matchedPrefixIndex < prefixSums.length - 1
|
||||
? matchedPrefixIndex
|
||||
: undefined
|
||||
}
|
||||
autoSubmitPending={autoSubmitTriggered}
|
||||
rejectedDigit={rejectedDigit}
|
||||
helpOverlay={
|
||||
!isSubmitting && feedback === 'none' && helpTermIndex !== null && helpContext ? (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
backgroundColor: isDark
|
||||
? 'rgba(30, 58, 138, 0.95)'
|
||||
: 'rgba(219, 234, 254, 0.95)',
|
||||
borderRadius: '12px',
|
||||
padding: '0.5rem',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
})}
|
||||
>
|
||||
{/* Term being helped indicator */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.25rem 0.75rem',
|
||||
backgroundColor: isDark ? 'purple.900' : 'purple.100',
|
||||
borderRadius: '20px',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
color: isDark ? 'purple.200' : 'purple.700',
|
||||
})}
|
||||
>
|
||||
<span>Adding:</span>
|
||||
<span className={css({ fontFamily: 'monospace', fontSize: '1rem' })}>
|
||||
{helpContext.term >= 0 ? '+' : ''}
|
||||
{helpContext.term}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Interactive abacus with arrows - just the abacus, no extra UI */}
|
||||
{/* Columns = max digits between current and target values (minimum 1) */}
|
||||
<PracticeHelpOverlay
|
||||
currentValue={helpContext.currentValue}
|
||||
targetValue={helpContext.targetValue}
|
||||
|
|
@ -1088,7 +1050,6 @@ export function ActiveSession({
|
|||
)}
|
||||
onTargetReached={handleTargetReached}
|
||||
/>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
|
@ -1108,6 +1069,98 @@ export function ActiveSession({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Right: Help panel with coach hint and decomposition (only in help mode) */}
|
||||
{!isSubmitting && feedback === 'none' && helpTermIndex !== null && helpContext && (
|
||||
<div
|
||||
data-element="help-panel"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: isDark ? 'blue.900' : 'blue.50',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid',
|
||||
borderColor: isDark ? 'blue.700' : 'blue.200',
|
||||
minWidth: '200px',
|
||||
maxWidth: '280px',
|
||||
})}
|
||||
>
|
||||
{/* Coach hint */}
|
||||
{(() => {
|
||||
const hint = generateCoachHint(helpContext.currentValue, helpContext.targetValue)
|
||||
if (!hint) return null
|
||||
return (
|
||||
<div
|
||||
data-element="coach-hint"
|
||||
className={css({
|
||||
padding: '0.5rem 0.75rem',
|
||||
backgroundColor: isDark ? 'gray.800' : 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'blue.800' : 'blue.100',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '0.875rem',
|
||||
color: isDark ? 'gray.300' : 'gray.700',
|
||||
lineHeight: '1.4',
|
||||
margin: 0,
|
||||
})}
|
||||
>
|
||||
{hint}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Decomposition display */}
|
||||
<div
|
||||
data-element="decomposition-display"
|
||||
className={css({
|
||||
padding: '0.5rem 0.75rem',
|
||||
backgroundColor: isDark ? 'gray.800' : 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'blue.800' : 'blue.100',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 'bold',
|
||||
color: isDark ? 'blue.300' : 'blue.600',
|
||||
marginBottom: '0.25rem',
|
||||
textTransform: 'uppercase',
|
||||
})}
|
||||
>
|
||||
Step-by-Step
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem',
|
||||
color: isDark ? 'gray.100' : 'gray.800',
|
||||
})}
|
||||
>
|
||||
<DecompositionProvider
|
||||
startValue={helpContext.currentValue}
|
||||
targetValue={helpContext.targetValue}
|
||||
currentStepIndex={0}
|
||||
abacusColumns={Math.max(
|
||||
1,
|
||||
Math.max(helpContext.currentValue, helpContext.targetValue).toString().length
|
||||
)}
|
||||
>
|
||||
<DecompositionDisplay />
|
||||
</DecompositionProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feedback message */}
|
||||
{feedback !== 'none' && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
'use client'
|
||||
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import {
|
||||
type AbacusOverlay,
|
||||
AbacusReact,
|
||||
|
|
@ -9,6 +8,7 @@ import {
|
|||
useAbacusDisplay,
|
||||
} from '@soroban/abacus-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { css } from '../../../styled-system/css'
|
||||
|
||||
/** Bead change from calculateBeadDiffFromValues */
|
||||
|
|
@ -213,6 +213,7 @@ export function HelpAbacus({
|
|||
colorScheme={abacusConfig.colorScheme}
|
||||
beadShape={abacusConfig.beadShape}
|
||||
hideInactiveBeads={abacusConfig.hideInactiveBeads}
|
||||
showNumbers={false} // Hide numerals to keep display tight
|
||||
soundEnabled={false} // Disable sound in help mode
|
||||
stepBeadHighlights={isAtTarget ? undefined : stepBeadHighlights}
|
||||
currentStep={currentStep}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
'use client'
|
||||
|
||||
/**
|
||||
* HelpCountdown - Pie chart style countdown timer for help escalation
|
||||
*
|
||||
* Shows a circular countdown that depletes as time passes, with seconds
|
||||
* remaining displayed in the center. Positioned in place of the "=" sign
|
||||
* in the VerticalProblem component.
|
||||
*/
|
||||
|
||||
import { css } from '../../../styled-system/css'
|
||||
|
||||
export interface HelpCountdownProps {
|
||||
/** Time elapsed since problem started (ms) */
|
||||
elapsedTimeMs: number
|
||||
/** Target time for next help level (ms) */
|
||||
targetTimeMs: number
|
||||
/** Size of the countdown circle (matches cell size) */
|
||||
size?: string
|
||||
/** Whether dark mode is active */
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color based on percentage remaining
|
||||
* Green (>66%) → Yellow (33-66%) → Orange (15-33%) → Red (<15%)
|
||||
*/
|
||||
function getColor(percentRemaining: number, isDark: boolean): string {
|
||||
if (percentRemaining > 66) {
|
||||
return isDark ? '#22c55e' : '#16a34a' // green
|
||||
} else if (percentRemaining > 33) {
|
||||
return isDark ? '#eab308' : '#ca8a04' // yellow
|
||||
} else if (percentRemaining > 15) {
|
||||
return isDark ? '#f97316' : '#ea580c' // orange
|
||||
} else {
|
||||
return isDark ? '#ef4444' : '#dc2626' // red
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HelpCountdown - Pie chart countdown timer
|
||||
*
|
||||
* Renders a circular progress indicator that depletes clockwise,
|
||||
* with the seconds remaining shown in the center.
|
||||
*/
|
||||
export function HelpCountdown({
|
||||
elapsedTimeMs,
|
||||
targetTimeMs,
|
||||
size = '2.4rem',
|
||||
isDark = false,
|
||||
}: HelpCountdownProps) {
|
||||
const remainingMs = Math.max(0, targetTimeMs - elapsedTimeMs)
|
||||
const remainingSeconds = Math.ceil(remainingMs / 1000)
|
||||
const percentRemaining = (remainingMs / targetTimeMs) * 100
|
||||
|
||||
// SVG parameters
|
||||
const viewBoxSize = 100
|
||||
const center = viewBoxSize / 2
|
||||
const radius = 40
|
||||
const circumference = 2 * Math.PI * radius
|
||||
|
||||
// Calculate stroke dash for remaining time (pie depletes as time passes)
|
||||
const strokeDashoffset = circumference * (1 - percentRemaining / 100)
|
||||
|
||||
const color = getColor(percentRemaining, isDark)
|
||||
|
||||
// Background color for the circle
|
||||
const bgColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return (
|
||||
<div
|
||||
data-element="help-countdown"
|
||||
data-seconds-remaining={remainingSeconds}
|
||||
className={css({
|
||||
width: size,
|
||||
height: size,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
||||
className={css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transform: 'rotate(-90deg)', // Start from top
|
||||
})}
|
||||
>
|
||||
{/* Background circle */}
|
||||
<circle cx={center} cy={center} r={radius} fill="none" stroke={bgColor} strokeWidth="12" />
|
||||
{/* Progress arc */}
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="12"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className={css({
|
||||
transition: 'stroke-dashoffset 0.3s ease-out, stroke 0.3s ease',
|
||||
})}
|
||||
/>
|
||||
</svg>
|
||||
{/* Seconds remaining in center */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
color: color,
|
||||
lineHeight: 1,
|
||||
})}
|
||||
>
|
||||
{remainingSeconds}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HelpCountdown
|
||||
|
|
@ -17,12 +17,8 @@ interface VerticalProblemProps {
|
|||
correctAnswer?: number
|
||||
/** Size variant */
|
||||
size?: 'normal' | 'large'
|
||||
/** Index of terms that have been confirmed (0 = first term done, 1 = first two terms done, etc.) */
|
||||
confirmedTermCount?: number
|
||||
/** Index of the term currently being helped with (highlighted) */
|
||||
/** Index of the term currently being helped with (shows arrow indicator) */
|
||||
currentHelpTermIndex?: number
|
||||
/** Detected prefix index - shows preview checkmarks/arrow before user clicks "Get Help" */
|
||||
detectedPrefixIndex?: number
|
||||
/** Whether auto-submit is about to trigger (shows celebration animation) */
|
||||
autoSubmitPending?: boolean
|
||||
/** Rejected digit to show as red X (null = no rejection) */
|
||||
|
|
@ -47,9 +43,7 @@ export function VerticalProblem({
|
|||
isCompleted = false,
|
||||
correctAnswer,
|
||||
size = 'normal',
|
||||
confirmedTermCount = 0,
|
||||
currentHelpTermIndex,
|
||||
detectedPrefixIndex,
|
||||
autoSubmitPending = false,
|
||||
rejectedDigit = null,
|
||||
helpOverlay,
|
||||
|
|
@ -125,89 +119,23 @@ export function VerticalProblem({
|
|||
const absValue = Math.abs(term)
|
||||
const digits = absValue.toString().padStart(maxDigits, ' ').split('')
|
||||
|
||||
// Term status for highlighting
|
||||
const isConfirmed = index < confirmedTermCount
|
||||
// Check if this term row should show the help overlay
|
||||
const isCurrentHelp = index === currentHelpTermIndex
|
||||
// Preview states - shown when user's input matches a prefix sum (before clicking "Get Help")
|
||||
const isPreviewConfirmed =
|
||||
detectedPrefixIndex !== undefined && index <= detectedPrefixIndex && !isConfirmed
|
||||
const isPreviewNext =
|
||||
detectedPrefixIndex !== undefined &&
|
||||
index === detectedPrefixIndex + 1 &&
|
||||
!isCurrentHelp &&
|
||||
detectedPrefixIndex < terms.length - 1 // Don't show if prefix is the full answer
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
data-element="term-row"
|
||||
data-term-status={
|
||||
isConfirmed
|
||||
? 'confirmed'
|
||||
: isCurrentHelp
|
||||
? 'current'
|
||||
: isPreviewConfirmed
|
||||
? 'preview-confirmed'
|
||||
: isPreviewNext
|
||||
? 'preview-next'
|
||||
: 'pending'
|
||||
}
|
||||
data-term-status={isCurrentHelp ? 'current' : 'pending'}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
position: 'relative',
|
||||
// Confirmed terms are dimmed with checkmark
|
||||
opacity: isConfirmed ? 0.5 : isPreviewConfirmed ? 0.7 : 1,
|
||||
// Current help term is highlighted
|
||||
backgroundColor: isCurrentHelp
|
||||
? isDark
|
||||
? 'purple.800'
|
||||
: 'purple.100'
|
||||
: isPreviewNext
|
||||
? isDark
|
||||
? 'yellow.900'
|
||||
: 'yellow.50'
|
||||
: 'transparent',
|
||||
borderRadius: isCurrentHelp || isPreviewNext ? '4px' : '0',
|
||||
padding: isCurrentHelp || isPreviewNext ? '2px 4px' : '0',
|
||||
marginLeft: isCurrentHelp || isPreviewNext ? '-4px' : '0',
|
||||
marginRight: isCurrentHelp || isPreviewNext ? '-4px' : '0',
|
||||
transition: 'all 0.2s ease',
|
||||
})}
|
||||
>
|
||||
{/* Checkmark for confirmed terms */}
|
||||
{isConfirmed && (
|
||||
<div
|
||||
data-element="confirmed-check"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '-1.5rem',
|
||||
color: isDark ? 'green.400' : 'green.500',
|
||||
fontSize: '0.875rem',
|
||||
})}
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview checkmark for detected prefix terms (shown in muted color with subtle pulse) */}
|
||||
{isPreviewConfirmed && (
|
||||
<div
|
||||
data-element="preview-check"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '-1.5rem',
|
||||
color: isDark ? 'yellow.400' : 'yellow.600',
|
||||
fontSize: '0.875rem',
|
||||
opacity: 0.8,
|
||||
})}
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Arrow indicator for current help term */}
|
||||
{/* Arrow indicator for current help term (the term being added) */}
|
||||
{isCurrentHelp && (
|
||||
<div
|
||||
data-element="current-arrow"
|
||||
|
|
@ -222,21 +150,6 @@ export function VerticalProblem({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview arrow for next term after detected prefix */}
|
||||
{isPreviewNext && (
|
||||
<div
|
||||
data-element="preview-arrow"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '-1.5rem',
|
||||
color: isDark ? 'yellow.400' : 'yellow.600',
|
||||
fontSize: '0.875rem',
|
||||
})}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Operator column (only show minus for negative) */}
|
||||
<div
|
||||
data-element="operator"
|
||||
|
|
@ -352,27 +265,19 @@ export function VerticalProblem({
|
|||
<span>Perfect!</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Equals sign column - show "..." for prefix sums (mathematically incomplete), "=" for final answer */}
|
||||
{/* Equals column */}
|
||||
<div
|
||||
data-element="equals"
|
||||
data-prefix-mode={detectedPrefixIndex !== undefined ? 'true' : undefined}
|
||||
className={css({
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color:
|
||||
detectedPrefixIndex !== undefined
|
||||
? isDark
|
||||
? 'yellow.400'
|
||||
: 'yellow.600'
|
||||
: isDark
|
||||
? 'gray.400'
|
||||
: 'gray.500',
|
||||
color: isDark ? 'gray.400' : 'gray.500',
|
||||
})}
|
||||
>
|
||||
{detectedPrefixIndex !== undefined ? '…' : '='}
|
||||
=
|
||||
</div>
|
||||
|
||||
{/* Answer digit cells - show maxDigits cells total */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Coach hint generator for practice help system
|
||||
*
|
||||
* Uses the same readable.summary from unifiedStepGenerator that the
|
||||
* tutorial CoachBar uses, ensuring consistent hints across the app.
|
||||
*/
|
||||
|
||||
import { generateUnifiedInstructionSequence } from '@/utils/unifiedStepGenerator'
|
||||
|
||||
/**
|
||||
* Generate a coach hint based on the current step
|
||||
*
|
||||
* Returns the segment's readable.summary if available, or null if not.
|
||||
* This matches the tutorial CoachBar behavior which only renders when
|
||||
* readable.summary exists.
|
||||
*/
|
||||
export function generateCoachHint(
|
||||
startValue: number,
|
||||
targetValue: number,
|
||||
currentStepIndex: number = 0
|
||||
): string | null {
|
||||
const sequence = generateUnifiedInstructionSequence(startValue, targetValue)
|
||||
|
||||
if (!sequence || sequence.steps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the current step
|
||||
const currentStep = sequence.steps[currentStepIndex]
|
||||
if (!currentStep) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Find the segment this step belongs to
|
||||
const segment = sequence.segments.find((s) => s.id === currentStep.segmentId)
|
||||
|
||||
// Return the segment's readable summary if available (same as tutorial CoachBar)
|
||||
return segment?.readable?.summary ?? null
|
||||
}
|
||||
Loading…
Reference in New Issue