🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
Some checks failed
Test Suite / test-backend (push) Has been cancelled
Test Suite / test-frontend (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / coverage-check (push) Has been cancelled

 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:
Renato
2026-03-31 11:27:11 -03:00
commit bc43c9e772
309 changed files with 84845 additions and 0 deletions

View 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>
);
}