🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
✨ Características: - 45 ejercicios universitarios (Basic → Advanced) - Renderizado LaTeX profesional - IA generativa (Z.ai/DashScope) - Docker 9 servicios - Tests 123/123 pasando - Seguridad enterprise (JWT, XSS, Rate limiting) 🐳 Infraestructura: - Next.js 14 + Node.js 20 - PostgreSQL 15 + Redis 7 - Docker Compose completo - Nginx + SSL ready 📚 Documentación: - 5 informes técnicos completos - README profesional - Scripts de deployment automatizados Estado: Producción lista ✅
This commit is contained in:
251
frontend/src/components/exercises/HintSystem.tsx
Normal file
251
frontend/src/components/exercises/HintSystem.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { MathBlock } from '@/components/math/MathFormula';
|
||||
import { Lightbulb, Eye, Sparkles } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Hint data interface
|
||||
*/
|
||||
export interface Hint {
|
||||
id: string;
|
||||
content: string;
|
||||
isMath?: boolean;
|
||||
cost?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* HintSystem component props
|
||||
*/
|
||||
export interface HintSystemProps {
|
||||
/**
|
||||
* Array of hints
|
||||
*/
|
||||
hints: Hint[];
|
||||
|
||||
/**
|
||||
* Current points
|
||||
*/
|
||||
currentPoints?: number;
|
||||
|
||||
/**
|
||||
* Points penalty per hint
|
||||
*/
|
||||
pointsPerHint?: number;
|
||||
|
||||
/**
|
||||
* Revealed hint indices
|
||||
*/
|
||||
revealedHints?: Set<number>;
|
||||
|
||||
/**
|
||||
* On hint reveal callback
|
||||
*/
|
||||
onRevealHint?: (index: number, cost: number) => void;
|
||||
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Compact mode
|
||||
*/
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* HintSystem component - displays hints with point costs
|
||||
*/
|
||||
export function HintSystem({
|
||||
hints,
|
||||
currentPoints,
|
||||
pointsPerHint = 5,
|
||||
revealedHints = new Set<number>(),
|
||||
onRevealHint,
|
||||
className,
|
||||
compact = false,
|
||||
}: HintSystemProps) {
|
||||
const [showAll] = useState(false);
|
||||
|
||||
if (hints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canAffordHint = (hintIndex: number) => {
|
||||
if (!currentPoints) return true;
|
||||
|
||||
const alreadyRevealed = Array.from(revealedHints).filter((i) => i <= hintIndex).length;
|
||||
const cost = (alreadyRevealed + 1) * pointsPerHint;
|
||||
return currentPoints >= cost;
|
||||
};
|
||||
|
||||
const getHintCost = (hintIndex: number) => {
|
||||
const alreadyRevealed = Array.from(revealedHints).filter((i) => i <= hintIndex).length;
|
||||
return (alreadyRevealed + 1) * pointsPerHint;
|
||||
};
|
||||
|
||||
const handleRevealHint = (index: number) => {
|
||||
if (onRevealHint) {
|
||||
onRevealHint(index, getHintCost(index));
|
||||
}
|
||||
};
|
||||
|
||||
const allRevealed = revealedHints.size === hints.length;
|
||||
const hasUnrevealed = hints.length > revealedHints.size;
|
||||
|
||||
return (
|
||||
<Card className={cn('border-yellow-200 dark:border-yellow-800', className)}>
|
||||
<CardHeader className={compact ? 'pb-3' : undefined}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Lightbulb className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
|
||||
<CardTitle className="text-lg">Hints</CardTitle>
|
||||
</div>
|
||||
|
||||
{hasUnrevealed && !compact && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{revealedHints.size}/{hints.length} revealed
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!compact && (
|
||||
<CardDescription>
|
||||
{currentPoints !== undefined && (
|
||||
<span>Cost: {pointsPerHint} points per hint (progressive)</span>
|
||||
)}
|
||||
{currentPoints === undefined && <span>Click to reveal hints</span>}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{hints.map((hint, index) => {
|
||||
const isRevealed = revealedHints.has(index);
|
||||
const cost = getHintCost(index);
|
||||
const canAfford = canAffordHint(index);
|
||||
|
||||
if (!showAll && !isRevealed && index > 0 && !revealedHints.has(index - 1)) {
|
||||
// Hide hints that are not yet available (sequential reveal)
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={hint.id}
|
||||
className={cn(
|
||||
'rounded-lg border p-4 transition-all duration-300',
|
||||
isRevealed
|
||||
? 'bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800'
|
||||
: 'bg-muted border-muted-foreground/20',
|
||||
isRevealed && 'animate-in fade-in slide-in-from-bottom-2 duration-300'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
'flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold',
|
||||
isRevealed
|
||||
? 'bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200'
|
||||
: 'bg-muted-foreground/20 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{isRevealed ? (
|
||||
<div className="animate-in fade-in duration-300 delay-100">
|
||||
{hint.isMath ? (
|
||||
<MathBlock formula={hint.content} />
|
||||
) : (
|
||||
<p className="text-sm text-yellow-900 dark:text-yellow-100">
|
||||
{hint.content}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hint.cost && (
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
<span>Cost: {hint.cost} points</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Hint #{index + 1} -{' '}
|
||||
{currentPoints !== undefined ? (
|
||||
canAfford ? (
|
||||
<span className="text-foreground">Reveal for {cost} points</span>
|
||||
) : (
|
||||
<span className="text-destructive">Need {cost - currentPoints} more points</span>
|
||||
)
|
||||
) : (
|
||||
<span className="text-foreground">Click to reveal</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{onRevealHint && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRevealHint(index)}
|
||||
disabled={!canAfford}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
Reveal Hint
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{allRevealed && (
|
||||
<div className="text-center py-2 animate-in fade-in duration-300">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Sparkles className="h-4 w-4 inline-block mr-1" />
|
||||
All hints revealed!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact hint button for inline display
|
||||
*/
|
||||
export interface HintButtonProps {
|
||||
hintCount: number;
|
||||
revealedCount: number;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HintButton({ hintCount, revealedCount, onClick, className }: HintButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
className={cn('gap-2', className)}
|
||||
>
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
<span>Hint</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({revealedCount}/{hintCount})
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user