Skip to main content

Overview

The learning activities system provides AI-generated exercises and exams for students to practice English. Activities are assigned after virtual classes and include automated grading with detailed solutions.

Activity Types

The platform supports four types of learning activities:
enum Type {
  exam
  audio
  video
  reading
}

enum Level {
  easy
  medium
  hard
}

enum ActivityStatus {
  uploaded
  pending
}
Comprehensive tests covering grammar, vocabulary, and comprehension.
  • Multiple question formats
  • Timed or untimed
  • Automated scoring

Task Model

Activities are stored in the Task model with AI-generated content:
model Task {
  id            String         @id @default(auto()) @map("_id") @db.ObjectId
  title         String
  content       String         // Markdown AI-generated questions
  solvedContent String         // Markdown answer AI-generated solutions
  description   String         // Short description about the task
  type          Type
  difficulty    Level          @default(medium)
  createdAt     DateTime       @default(now())
  updatedAt     DateTime       @updatedAt
  UserActivity  UserActivity[]
}
Content Format: Both content and solvedContent use Markdown format for rich text formatting, tables, code blocks, and structured exercises.

Activity Flow

1. Activity Assignment

Activities are linked to virtual classes through the UserActivity junction table:
// Create user activity after class scheduling
if (newClass) {
  await db.userActivity.create({
    data: {
      userId: newClass.bookedById,
      classId: newClass.id,
      taskId: null,  // Assigned later
      rol: 'anfitrion',
      completed: false
    }
  });
}

2. Activity Page

Students can view activity details before starting:
const ActividadPage = async ({ params }: Props) => {
  const resolvedParams = await params;
  const task = await getTask(resolvedParams.actividadId);
  
  if (!task) return notFound();

  return (
    <section className="border max-w-6xl px-2 py-4 space-y-4">
      <h1>Actividad: {task.title}</h1>
      
      <div>
        <p>Instrucciones:</p>
        <p>Estas por entrar a resolver la actividad, podes completarla 
           cuando vos quieras y todas las veces que necesites. A los 90 
           minutos despues de iniciada la actividad tendras acceso a un 
           documentos con las resolucion correcta para que puedas comparar 
           tus respuestas</p>
      </div>
      
      <div>
        <p>Descripciones:</p>
        <ReactMarkdown
          components={components}
          remarkPlugins={[remarkGfm]}
        >
          {task.description}
        </ReactMarkdown>
      </div>
      
      <div>
        <p>Dificultad: {translateDifficulty(task.difficulty)}</p>
        <p>Tipo: {translateType(task.type)}</p>
      </div>
      
      <a href={`/actividad/${task.id}/resolver`}>
        Comenzar actividad
      </a>
    </section>
  );
};

3. Activity Resolver

The resolver page displays the activity content and allows students to work through it:
const ResolverActividad = async ({ params }: Props) => {
  const resolvedParams = await params;
  const task = await getTask(resolvedParams.actividadId);
  
  // Mark task as started
  await markTaskComplete(resolvedParams.actividadId);

  return (
    <section className="border max-w-6xl px-1 py-4 space-y-4">
      {/* Answer sidebar with solutions (timed reveal) */}
      <AnswerSidebar 
        taskSolved={task?.solvedContent} 
        updatedAt={task?.updatedAt} 
      />
      
      {/* Main activity content */}
      <ExamMarkdown content={task?.content} />
    </section>
  );
};

Markdown Rendering

Activities use custom Markdown components for enhanced formatting:
const components: Record<string, React.ComponentType<any>> = {
  // Tables
  table: ({ children }) => (
    <table className="w-full border-collapse my-4 rounded-lg">
      {children}
    </table>
  ),
  thead: ({ children }) => (
    <thead className="bg-yellow-50 dark:bg-black/30">
      {children}
    </thead>
  ),
  th: ({ children }) => (
    <th className="text-left font-semibold px-4 py-2 border-b">
      {children}
    </th>
  ),
  tbody: ({ children }) => <tbody>{children}</tbody>,
  tr: ({ children }) => <tr>{children}</tr>,
  td: ({ children }) => (
    <td className="p-4 text-xs border-b">
      {children}
    </td>
  ),

  // Code blocks
  code: ({ children }) => (
    <code className="bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">
      {children}
    </code>
  ),
  pre: ({ children }) => (
    <pre className="bg-gray-900 text-gray-100 p-3 rounded-md overflow-x-auto">
      {children}
    </pre>
  ),
};

<ReactMarkdown
  components={components}
  remarkPlugins={[remarkGfm]}
>
  {task.content}
</ReactMarkdown>
The remarkGfm plugin enables GitHub Flavored Markdown, supporting tables, task lists, and strikethrough text.

Activity Status Tracking

Virtual classes track whether students have completed their assigned activities:
model VirtualClass {
  // ...
  activityStatus ActivityStatus @default(pending)
  // ...
}

model UserActivity {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  userId    String   @db.ObjectId
  taskId    String?  @db.ObjectId  // Links to Task
  classId   String   @db.ObjectId  // Links to VirtualClass
  rol       classRole
  completed Boolean? @default(false)  // Activity completion status
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Answer Reveal System

The AnswerSidebar component reveals solutions after 90 minutes:
interface AnswerSidebarProps {
  taskSolved: string;  // AI-generated solutions in Markdown
  updatedAt: Date;     // Task start timestamp
}

const AnswerSidebar = ({ taskSolved, updatedAt }: AnswerSidebarProps) => {
  const [timeElapsed, setTimeElapsed] = useState(0);
  const REVEAL_TIME = 90 * 60 * 1000; // 90 minutes in milliseconds
  
  useEffect(() => {
    const interval = setInterval(() => {
      const elapsed = Date.now() - new Date(updatedAt).getTime();
      setTimeElapsed(elapsed);
    }, 1000);
    
    return () => clearInterval(interval);
  }, [updatedAt]);
  
  const canViewAnswers = timeElapsed >= REVEAL_TIME;
  
  return (
    <aside>
      {canViewAnswers ? (
        <ReactMarkdown>{taskSolved}</ReactMarkdown>
      ) : (
        <p>Respuestas disponibles en {formatTime(REVEAL_TIME - timeElapsed)}</p>
      )}
    </aside>
  );
};

AI Content Generation

Activities use AI-generated content in Markdown format. Here’s an example structure:
# English Grammar Exercise

## Part 1: Fill in the Blanks

Complete the sentences with the correct form of the verb in parentheses.

1. She _________ (go) to the store every Saturday.
2. They _________ (watch) a movie when I called.
3. He _________ (live) in London since 2015.

## Part 2: Multiple Choice

Choose the correct answer:

**Question 1:** What is the past tense of "run"?
- A) runned
- B) ran
- C) running
- D) runs

**Question 2:** Which sentence is correct?
- A) He don't like coffee.
- B) He doesn't likes coffee.
- C) He doesn't like coffee.
- D) He not like coffee.

## Part 3: Reading Comprehension

Read the passage and answer the questions:

> John wakes up at 6 AM every morning. He goes for a jog in the park 
> before having breakfast. After breakfast, he drives to work...

1. What time does John wake up?
2. What does he do before breakfast?

Difficulty Levels

The system supports three difficulty levels:
  • Basic vocabulary and grammar
  • Simple sentence structures
  • Clear, direct questions
  • Shorter reading passages

Utility Functions

Helper functions translate database enums to Spanish:
export function translateDifficulty(difficulty: Level): string {
  const translations = {
    easy: 'Fácil',
    medium: 'Medio',
    hard: 'Difícil'
  };
  return translations[difficulty];
}

export function translateType(type: Type): string {
  const translations = {
    exam: 'Examen',
    audio: 'Audio',
    video: 'Video',
    reading: 'Lectura'
  };
  return translations[type];
}

Database Queries

Common queries for activity management:
// Get single task
export async function getTask(taskId: string) {
  const task = await db.task.findUnique({
    where: { id: taskId },
    include: {
      UserActivity: {
        include: {
          user: true,
          class: true
        }
      }
    }
  });
  return task;
}

// Mark task as complete
export async function markTaskComplete(taskId: string) {
  await db.userActivity.updateMany({
    where: { taskId },
    data: { completed: true }
  });
}

// Get user's activities
export async function getUserActivities(userId: string) {
  const activities = await db.userActivity.findMany({
    where: { userId },
    include: {
      task: true,
      class: true
    },
    orderBy: { createdAt: 'desc' }
  });
  return activities;
}

Activity Display

The class card shows activity status:
<Card className='flex border border-card'>
  {/* ...class details... */}
  
  <p className='font-bold xl:font-normal capitalize'>
    <span className='block lg:hidden'>Tarea IA: </span>
    {classItem.activityStatus === 'uploaded' 
      ? 'Realizada' 
      : 'No Realizada'
    }
  </p>
</Card>

Integration with Virtual Classes

Activities are assigned based on class completion:
// After a virtual class ends, assign activity
async function assignPostClassActivity(
  classId: string, 
  userId: string, 
  userLevel: NivelIngles
) {
  // Select appropriate difficulty based on user level
  const difficulty = {
    inicial: 'easy',
    basico: 'easy',
    intermedio: 'medium',
    avanzado: 'hard'
  }[userLevel];
  
  // Find or create suitable task
  const task = await db.task.findFirst({
    where: {
      difficulty: difficulty,
      type: 'exam'  // Default to exam type
    }
  });
  
  // Link task to user activity
  await db.userActivity.updateMany({
    where: {
      classId,
      userId,
      taskId: null
    },
    data: {
      taskId: task.id
    }
  });
  
  // Update class activity status
  await db.virtualClass.update({
    where: { id: classId },
    data: { activityStatus: 'uploaded' }
  });
}

Best Practices

Content Quality

  • Use clear, grammatically correct English in all activities
  • Provide detailed explanations in solutions
  • Include context and examples in answer keys
  • Test activities before assigning to students

Difficulty Matching

  • Align activity difficulty with user’s English level
  • Start easier and progressively increase complexity
  • Consider class focus when selecting activity types
  • Track completion rates to adjust difficulty

Markdown Best Practices

  • Use headers to organize sections
  • Leverage tables for structured data
  • Include code blocks for grammar rules or examples
  • Add emphasis with bold and italics

API Endpoints

Get Task

GET /api/tasks/:taskId

Response:
{
  "id": "507f1f77bcf86cd799439011",
  "title": "Present Perfect Tense Exercise",
  "description": "Practice using present perfect tense",
  "type": "exam",
  "difficulty": "medium",
  "content": "# Exercise...\n\n## Part 1...",
  "solvedContent": "# Answer Key...\n\n## Part 1...",
  "createdAt": "2026-03-01T10:00:00Z",
  "updatedAt": "2026-03-01T10:00:00Z"
}

Mark Activity Complete

POST /api/activities/:activityId/complete

Response:
{
  "success": true,
  "completed": true,
  "completedAt": "2026-03-01T11:30:00Z"
}

Build docs developers (and LLMs) love