diff --git a/src/accounts/templates/accounts/login.html b/src/accounts/templates/accounts/login.html index 8e13125..a42846e 100644 --- a/src/accounts/templates/accounts/login.html +++ b/src/accounts/templates/accounts/login.html @@ -3,6 +3,90 @@ {% block title %}Login{% endblock %} {% block content %} +{% if not user.is_authenticated %} +
+
+ + LL + + LLTeacher + Interactive homework with AI-guided reasoning. + + + +
+ +
+
+
+
Welcome back
+

Sign in to keep working.

+

+ Pick up where you left off. Continue an assignment, review your + explanation, or open a conversation with the AI tutor. +

+
+ +
+

Login

+

Use your LLTeacher account.

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + +
+ + {{ form.username }} + {% if form.username.errors %} +
+ {{ form.username.errors|join:", " }} +
+ {% endif %} +
+ +
+ + {{ form.password }} + {% if form.password.errors %} +
+ {{ form.password.errors|join:", " }} +
+ {% endif %} +
+ + + + + + +
+ +
+ Don't have an account? Register +
+
+
+
+ + +
+ +{% else %}
@@ -13,7 +97,7 @@

Login

{% csrf_token %} - + {% if form.non_field_errors %}
{% for error in form.non_field_errors %} @@ -21,8 +105,7 @@

Login

{% endfor %}
{% endif %} - - +
{{ form.username }} @@ -32,8 +115,7 @@

Login

{% endif %}
- - +
{{ form.password }} @@ -43,16 +125,13 @@

Login

{% endif %}
- - + - - +
- - +
+{% endif %} {% endblock %} diff --git a/src/accounts/templates/accounts/profile.html b/src/accounts/templates/accounts/profile.html index 20a52b2..b420c7d 100644 --- a/src/accounts/templates/accounts/profile.html +++ b/src/accounts/templates/accounts/profile.html @@ -3,6 +3,218 @@ {% block title %}User Profile{% endblock %} {% block content %} +{% if profile_data.role == 'student' and request.user.student_profile and not request.user.teacher_profile %} +{% comment %}STUDENT branch — dark modern profile{% endcomment %} +
+ {% include "student/_sidebar.html" with active="profile" %} + +
+
+ +
+ +
+
+ +
+
+
+ +{% elif request.user.teacher_profile %} +{% comment %}TEACHER branch — dark modern profile shell{% endcomment %} +
+ {% include "teacher/_sidebar.html" with active="profile" %} + +
+
+ +
+ +
+
+ +
+
+
+
Account
+

+ {% if profile_data.first_name or profile_data.last_name %} + {{ profile_data.first_name }} {{ profile_data.last_name }} + {% else %} + {{ profile_data.username }} + {% endif %} +

+

@{{ profile_data.username }} · Instructor · Member since {{ profile_data.joined_date|date:"F j, Y" }}

+
+
+ Instructor +
+
+ +
+
+
Courses created
+
{{ profile_data.courses_created|default:0 }}
+
Across your account
+
+
+
Role
+
{{ profile_data.role|title }}
+
Current account role
+
+
+
Joined
+
{{ profile_data.joined_date|date:"M Y" }}
+
Member since
+
+
+
Username
+
@{{ profile_data.username }}
+
Login identifier
+
+
+ +
+
Edit profile
+

Personal information

+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %}
{{ error }}
{% endfor %} +
+ {% endif %} + +
+
+ + {{ form.first_name }} + {% if form.first_name.errors %}
{{ form.first_name.errors.0 }}
{% endif %} +
+
+ + {{ form.last_name }} + {% if form.last_name.errors %}
{{ form.last_name.errors.0 }}
{% endif %} +
+
+ + {{ form.email }} + {% if form.email.errors %}
{{ form.email.errors.0 }}
{% endif %} +
+
+ +
+ + Cancel +
+
+
+
+
+
+ +{% else %} +{% comment %}TA / fallback branch — original markup{% endcomment %}
@@ -59,7 +271,7 @@
Completed Sections

Edit Profile

{% csrf_token %} - + {% if form.non_field_errors %}
{% for error in form.non_field_errors %} @@ -109,4 +321,5 @@

Edit Profile

-{% endblock %} \ No newline at end of file +{% endif %} +{% endblock %} diff --git a/src/accounts/templates/accounts/register.html b/src/accounts/templates/accounts/register.html index b42a550..428af0b 100644 --- a/src/accounts/templates/accounts/register.html +++ b/src/accounts/templates/accounts/register.html @@ -3,6 +3,121 @@ {% block title %}Register{% endblock %} {% block content %} +{% if not user.is_authenticated %} +
+
+ + LL + + LLTeacher + Interactive homework with AI-guided reasoning. + + + +
+ +
+
+
+
Get started
+

Create your LLTeacher account.

+

+ Set up a free account to access homework assignments, work + through problems with reasoning prompts, and discuss your + explanations with the AI tutor. +

+
+ +
+

Create an Account

+

It only takes a minute.

+ + + {% csrf_token %} + + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + +
+ + {{ form.email }} + {% if form.email.errors %} +
+ {{ form.email.errors|join:", " }} +
+ {% endif %} + + Please use your University of Washington email address (@uw.edu) + +
+ +
+
+ + {{ form.first_name }} + {% if form.first_name.errors %} +
+ {{ form.first_name.errors|join:", " }} +
+ {% endif %} +
+
+ + {{ form.last_name }} + {% if form.last_name.errors %} +
+ {{ form.last_name.errors|join:", " }} +
+ {% endif %} +
+
+ +
+ + {{ form.password1 }} + {% if form.password1.errors %} +
+ {{ form.password1.errors|join:", " }} +
+ {% endif %} +
{{ form.password1.help_text }}
+
+ +
+ + {{ form.password2 }} + {% if form.password2.errors %} +
+ {{ form.password2.errors|join:", " }} +
+ {% endif %} +
{{ form.password2.help_text }}
+
+ + + + +
+ Already have an account? Log in +
+
+
+
+ + +
+ +{% else %}
@@ -13,7 +128,7 @@

Create an Account

{% csrf_token %} - + {% if form.non_field_errors %}
{% for error in form.non_field_errors %} @@ -21,8 +136,7 @@

Create an Account

{% endfor %}
{% endif %} - - +
{{ form.email }} @@ -35,7 +149,7 @@

Create an Account

Please use your University of Washington email address (@uw.edu)
- +
@@ -56,8 +170,7 @@

Create an Account

{% endif %}
- - +
{{ form.password1 }} @@ -68,7 +181,7 @@

Create an Account

{% endif %}
{{ form.password1.help_text }}
- +
{{ form.password2 }} @@ -79,8 +192,7 @@

Create an Account

{% endif %}
{{ form.password2.help_text }}
- - +
@@ -93,4 +205,5 @@

Create an Account

+{% endif %} {% endblock %} diff --git a/src/conversations/forms.py b/src/conversations/forms.py new file mode 100644 index 0000000..1095f9d --- /dev/null +++ b/src/conversations/forms.py @@ -0,0 +1,32 @@ +"""Forms for the conversations app.""" + +from django import forms + +from .models import TeacherFeedback + + +class TeacherFeedbackForm(forms.ModelForm): + """Form for teachers to submit/update feedback on a student conversation.""" + + class Meta: + model = TeacherFeedback + fields = ["feedback_type", "feedback"] + widgets = { + "feedback": forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control", + "placeholder": ( + "Write feedback for the student — what is strong, " + "what needs revision, and any next steps." + ), + } + ), + "feedback_type": forms.Select(attrs={"class": "form-select"}), + } + + def clean_feedback(self): + value = (self.cleaned_data.get("feedback") or "").strip() + if not value: + raise forms.ValidationError("Feedback cannot be empty.") + return value diff --git a/src/conversations/migrations/0007_teacherfeedback.py b/src/conversations/migrations/0007_teacherfeedback.py new file mode 100644 index 0000000..0c51658 --- /dev/null +++ b/src/conversations/migrations/0007_teacherfeedback.py @@ -0,0 +1,111 @@ +# Generated by Django 5.2.13 on 2026-05-23 00:24 + +import django.core.validators +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "conversations", + "0006_alter_homeworkprogresswidgetresponse_post_value_and_more", + ), + ("homeworks", "0008_homeworkprogresswidget"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="TeacherFeedback", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "feedback", + models.TextField( + validators=[django.core.validators.MinLengthValidator(1)] + ), + ), + ( + "feedback_type", + models.CharField( + choices=[ + ("general", "General"), + ("needs_revision", "Needs revision"), + ("good_work", "Good work"), + ("clarify_reasoning", "Clarify reasoning"), + ], + default="general", + max_length=32, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "conversation", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="teacher_feedback", + to="conversations.conversation", + ), + ), + ( + "section", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="teacher_feedback", + to="homeworks.section", + ), + ), + ( + "student", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_feedback", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "submission", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="teacher_feedback", + to="conversations.submission", + ), + ), + ( + "teacher", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="authored_feedback", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "conversations_teacher_feedback", + "ordering": ["-updated_at"], + "constraints": [ + models.UniqueConstraint( + condition=models.Q(("conversation__isnull", False)), + fields=("teacher", "conversation"), + name="uniq_teacher_feedback_per_conversation", + ) + ], + }, + ), + ] diff --git a/src/conversations/models.py b/src/conversations/models.py index 17fb524..5961002 100644 --- a/src/conversations/models.py +++ b/src/conversations/models.py @@ -280,3 +280,80 @@ def save(self, *args, **kwargs): if self.post_value is not None and self.post_submitted_at is None: self.post_submitted_at = timezone.now() super().save(*args, **kwargs) + + +class TeacherFeedback(models.Model): + """Teacher-authored feedback on a student's conversation/section. + + Visible to the student who owns the conversation and to teachers/TAs of + the same course context. One feedback per (teacher, conversation) pair; + teachers can update their own feedback. + """ + + FEEDBACK_TYPE_GENERAL = "general" + FEEDBACK_TYPE_NEEDS_REVISION = "needs_revision" + FEEDBACK_TYPE_GOOD_WORK = "good_work" + FEEDBACK_TYPE_CLARIFY_REASONING = "clarify_reasoning" + + FEEDBACK_TYPE_CHOICES = [ + (FEEDBACK_TYPE_GENERAL, "General"), + (FEEDBACK_TYPE_NEEDS_REVISION, "Needs revision"), + (FEEDBACK_TYPE_GOOD_WORK, "Good work"), + (FEEDBACK_TYPE_CLARIFY_REASONING, "Clarify reasoning"), + ] + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + teacher = models.ForeignKey( + "accounts.User", + on_delete=models.CASCADE, + related_name="authored_feedback", + ) + student = models.ForeignKey( + "accounts.User", + on_delete=models.CASCADE, + related_name="received_feedback", + ) + section = models.ForeignKey( + "homeworks.Section", + on_delete=models.CASCADE, + related_name="teacher_feedback", + ) + conversation = models.ForeignKey( + Conversation, + on_delete=models.CASCADE, + related_name="teacher_feedback", + null=True, + blank=True, + ) + submission = models.ForeignKey( + Submission, + on_delete=models.SET_NULL, + related_name="teacher_feedback", + null=True, + blank=True, + ) + feedback = models.TextField(validators=[MinLengthValidator(1)]) + feedback_type = models.CharField( + max_length=32, + choices=FEEDBACK_TYPE_CHOICES, + default=FEEDBACK_TYPE_GENERAL, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "conversations_teacher_feedback" + ordering = ["-updated_at"] + constraints = [ + models.UniqueConstraint( + fields=["teacher", "conversation"], + condition=models.Q(conversation__isnull=False), + name="uniq_teacher_feedback_per_conversation", + ), + ] + + def __str__(self): + return ( + f"Feedback by {self.teacher.username} for " + f"{self.student.username} on section {self.section_id}" + ) diff --git a/src/conversations/templates/conversations/detail.html b/src/conversations/templates/conversations/detail.html index dce160b..b6eecd8 100644 --- a/src/conversations/templates/conversations/detail.html +++ b/src/conversations/templates/conversations/detail.html @@ -9,6 +9,478 @@ {% endblock %} {% block content %} +{% if request.user.student_profile and not request.user.teacher_profile and not conversation_data.is_teacher_test %} +{% comment %}STUDENT branch — wraps existing chat surface in shell. JS hooks preserved.{% endcomment %} +
+ {% include "student/_sidebar.html" with active="homework" %} + +
+
+ +
+ +
+
+ +
+
+
+
AI Tutor conversation
+

{{ conversation_data.section_title }}

+

Work through your reasoning with the AI tutor before submitting.

+
+ {% if user.id == conversation_data.user_id %} +
+ + {% csrf_token %} + + + {% if conversation_data.can_submit %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ {% endif %} +
+ +
+
+ {# ====== EXISTING CHAT SURFACE — JS hooks must stay intact ====== #} +
+ {% if timeline %} + {% for item in timeline %} + {% if item.type == 'message' %} + {% with message=item.data %} +
+
+ {% if message.is_from_student %} + Student + {% elif message.is_from_ai %} + AI Tutor + {% elif message.is_system_message %} + System + {% else %} + Other + {% endif %} + {{ message.timestamp|date:"M d, Y H:i" }} +
+
+ {% if message.message_type == 'code' %} +
+
+ + + R Code + + +
+
{{ message.content }}
+
+
+ {% elif message.message_type == 'code_execution' %} +
{{ message.content }}
+ {% else %} + + + + {% endif %} +
+
+ {% endwith %} + {% endif %} + {% endfor %} + {% else %} +
+ Send your first message to start the conversation. +
+ {% endif %} +
+
+ + {# Message input form — students only #} + {% if user.id == conversation_data.user_id %} +
+
+ {% csrf_token %} +
+ + +
+
+
+
+ + + + + +
+
+ +
+
+
+ {% endif %} +
+ + {% if teacher_feedback_list %} +
+
Teacher Feedback
+

From your instructor

+
+ {% for fb in teacher_feedback_list %} +
+
+ {{ fb.teacher.username }} + + {{ fb.get_feedback_type_display }} + + {{ fb.updated_at|date:"M d, Y H:i" }} +
+

{{ fb.feedback }}

+
+ {% endfor %} +
+
+ {% endif %} + + {% if conversation_data.homework_id %} + + {% else %} + + {% endif %} +
+
+
+ +{% elif request.user.teacher_profile %} +{% comment %}TEACHER branch — dark modern conversation workspace. JS hooks preserved.{% endcomment %} +
+ {% include "teacher/_sidebar.html" with active="homework" %} + +
+
+ +
+ +
+
+ +
+
+
+
+ {% if conversation_data.is_teacher_test %}Instructor test conversation{% else %}Student conversation{% endif %} +
+

{{ conversation_data.section_title }}

+

+ {% if conversation_data.is_teacher_test %} + Test conversation under your account. Use it to verify prompts and AI responses. + {% else %} + Read-only view of a student's AI tutor conversation for this section. + {% endif %} +

+
+ {% if not conversation_data.is_teacher_test and user.id == conversation_data.user_id %} +
+
+ {% csrf_token %} + +
+ {% if conversation_data.can_submit %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ {% endif %} +
+ + {% if conversation_data.is_teacher_test %} +
+ You're viewing this conversation as a teacher. +
+ {% endif %} + + {% if conversation_data.paste_events and user.id != conversation_data.user_id %} +
+ {{ conversation_data.paste_events|length }} paste event{{ conversation_data.paste_events|length|pluralize }} detected + in this conversation (shown inline below). +
+ {% endif %} + +
+
+ {# ====== EXISTING CHAT SURFACE — JS hooks must stay intact ====== #} +
+ {% if timeline %} + {% for item in timeline %} + {% if item.type == 'message' %} + {% with message=item.data %} +
+
+ {% if message.is_from_student %} + Student + {% elif message.is_from_ai %} + AI Tutor + {% elif message.is_system_message %} + System + {% else %} + Other + {% endif %} + {{ message.timestamp|date:"M d, Y H:i" }} +
+
+ {% if message.message_type == 'code' %} +
+
+ + + R Code + + +
+
{{ message.content }}
+
+
+ {% elif message.message_type == 'code_execution' %} +
{{ message.content }}
+ {% else %} + + + + {% endif %} +
+
+ {% endwith %} + {% elif item.type == 'paste_event' %} + {% with paste_event=item.data %} +
+
+ PASTE DETECTED + + {{ paste_event.timestamp|date:"M d, Y H:i" }} | + {{ paste_event.word_count }} words ({{ paste_event.content_length }} chars) + +
+
+
+ Click to view pasted content +
{{ paste_event.pasted_content }}
+
+
+
+ {% endwith %} + {% elif item.type == 'rapid_text_growth_event' %} + {% with rapid_text_growth_event=item.data %} +
+
+ RAPID TEXT GROWTH DETECTED + + {{ rapid_text_growth_event.timestamp|date:"M d, Y H:i" }} | + {{ rapid_text_growth_event.added_text|length }} chars + +
+
+
+ Click to view added text +
{{ rapid_text_growth_event.added_text }}
+
+
+
+ {% endwith %} + {% endif %} + {% endfor %} + {% else %} +
No messages in this conversation yet.
+ {% endif %} +
+
+ + {# Message composer — only if this is the teacher's own conversation #} + {% if user.id == conversation_data.user_id %} +
+
+ {% csrf_token %} +
+ + +
+
+
+
+ + + + +
+
+ +
+
+
+ {% elif conversation_data.is_teacher_test %} +
+ You are viewing this conversation as a teacher and cannot send messages. +
+ {% else %} +
+ Conversation review is read-only. +
+ {% endif %} +
+ + {# Teacher feedback — saved entries and submission form #} + {% if teacher_feedback_list or can_submit_feedback %} +
+
Teacher Feedback
+

Feedback on this conversation

+ + {% if teacher_feedback_list %} +
+ {% for fb in teacher_feedback_list %} +
+
+ {{ fb.teacher.username }} + + {{ fb.get_feedback_type_display }} + + {{ fb.updated_at|date:"M d, Y H:i" }} +
+

{{ fb.feedback }}

+
+ {% endfor %} +
+ {% endif %} + + {% if can_submit_feedback %} +
+ {% csrf_token %} +
+
+ + {{ feedback_form.feedback_type }} +
+
+ + {{ feedback_form.feedback }} + {% if feedback_form.feedback.errors %} +
{{ feedback_form.feedback.errors.0 }}
+ {% endif %} +
+
+
+ +
+
+ {% endif %} +
+ {% endif %} + + {% if conversation_data.homework_id %} + + {% else %} + + {% endif %} +
+
+
+ +{% else %} +{% comment %}TA / fallback branch — original markup{% endcomment %}
@@ -61,7 +533,7 @@

Conversation for {{ conversation_data.section_title }}

{{ conversation_data.paste_events|length }} paste event{{ conversation_data.paste_events|length|pluralize }} detected in this conversation (shown inline below).
{% endif %} - +
{% if timeline %} @@ -164,7 +636,7 @@

Conversation for {{ conversation_data.section_title }}

{% endif %}
- + {% if user.id == conversation_data.user_id %}
@@ -184,7 +656,7 @@

Conversation for {{ conversation_data.section_title }}

- +
{% elif conversation_data.is_teacher_test %}
- + You are viewing this conversation as a teacher and cannot send messages.
{% endif %}
- + {% if conversation_data.homework_id %}
@@ -225,6 +697,7 @@

Conversation for {{ conversation_data.section_title }}

+{% endif %} +{% if 'student' in data.user_roles and 'teacher' not in data.user_roles %} + +{% endif %} {% endblock %} {% block content %} +{% if 'student' in data.user_roles and 'teacher' not in data.user_roles %} +{% comment %}STUDENT branch — dark modern workspace{% endcomment %} +
+ {% include "student/_sidebar.html" with active="homework" %} + +
+
+ +
+ +
+
+ +
+
+
+
Section {{ data.section_order }} · {{ data.homework_title }}
+

{{ data.section_title }}

+

+ Work through the problem, write your reasoning, and discuss with the AI tutor before submitting. +

+
+
+ {% if data.submission %} + Submitted + {% elif data.section_type == 'non_interactive' %} + Open answer + {% else %} + In progress + {% endif %} +
+
+ +
+
+ {# Problem prompt #} +
+
Problem
+
+ + + +
+
+ + {# Reasoning workspace — interactive sections #} + {% if data.section_type != 'non_interactive' %} +
+
Your reasoning
+
Explain how you got your answer
+

+ Before submitting, write a short explanation of how you worked through this section. + Mention the steps, the formula or rule you used, and why your answer follows. + Your reasoning helps the AI tutor give better feedback. +

+ + + + + + + +
+ + + + Discuss with AI tutor + +
+ + +
+ {% endif %} + + {# Non-interactive: existing answer form #} + {% if data.section_type == 'non_interactive' %} +
+
Your answer
+

Submit your answer

+
+ {% csrf_token %} + +
+ +
+
+
+ + {% if data.existing_answers %} +
+
Previous answers
+

Your past submissions

+ {% for ans in data.existing_answers %} +
+
+ {{ ans.submitted_at|date:"F d, Y H:i" }} +
+

{{ ans.answer }}

+
+ {% endfor %} +
+ {% endif %} + {% endif %} + + {# Submission status — interactive sections #} + {% if data.section_type != 'non_interactive' %} +
+
Submission
+ {% if data.submission %} +

Submitted

+

+ You submitted this section on {{ data.submission.submitted_at|date:"F d, Y H:i" }}. +

+ {% else %} +

Not submitted yet

+

+ Once you've worked through your reasoning with the AI tutor, you can submit from the conversation page. +

+ + Start a conversation + + + {% endif %} +
+ + {# Existing conversations list #} + {% if data.conversations %} +
+
Your conversations
+

Continue or review

+ +
+ {% endif %} + {% endif %} + + +
+ + {# AI Tutor side panel — interactive sections only #} + {% if data.section_type != 'non_interactive' %} + + {% endif %} +
+
+
+
+ +{% elif 'teacher' in data.user_roles %} +{% comment %}TEACHER branch — dark modern instructor shell{% endcomment %} +
+ {% include "teacher/_sidebar.html" with active="homework" %} + +
+
+ +
+ +
+
+ +
+
+
+
Section {{ data.section_order }} · {{ data.homework_title }}
+

{{ data.section_title }}

+

+ Review the prompt, the solution, and any student conversations for this section. +

+
+
+ Teaching + {% if data.section_type == 'non_interactive' %} + Non-Interactive + {% else %} + Interactive + {% endif %} +
+
+ +
+
Prompt
+
+ + + +
+
+ + {% if data.has_solution %} +
+
Solution · Teachers only
+
+ + + +
+
+ {% endif %} + +
+
+
+
Conversations
+

Student discussions on this section

+
+ New conversation +
+ + {% if data.conversations %} +
+ {% for conversation in data.conversations %} + + + + +
+ {{ conversation.label }} +
{{ conversation.message_count }} message{{ conversation.message_count|pluralize }} · updated {{ conversation.updated_at|date:"M d, Y H:i" }}
+
+ {% if conversation.role == 'teacher' %} + Teaching + {% elif conversation.role == 'student' %} + Student + {% elif conversation.role == 'teacher_assistant' %} + TA + {% endif %} +
+ {% endfor %} +
+ {% else %} +

No conversations started yet for this section.

+ {% endif %} +
+ +
+ + + Back to homework + +
+
+
+
+ +{% else %} +{% comment %}TA / fallback branch — original markup{% endcomment %}
{% if 'teacher_assistant' in data.user_roles and 'teacher' not in data.user_roles and 'student' not in data.user_roles %}
@@ -83,7 +421,7 @@

Conversations

{% if data.conversations %} +{% endif %} {% endblock %} diff --git a/src/homeworks/templates/homeworks/submissions.html b/src/homeworks/templates/homeworks/submissions.html index a12fa6f..e62ce79 100644 --- a/src/homeworks/templates/homeworks/submissions.html +++ b/src/homeworks/templates/homeworks/submissions.html @@ -3,297 +3,267 @@ {% block title %}{{ data.homework_title }} - Submissions{% endblock %} {% block content %} -
- -
-
-

{{ data.homework_title }} - Submissions

-

Due: {{ data.homework_due_date|date:"F d, Y" }} | {{ data.total_sections }} sections

-
- -
+{% if request.user.teacher_profile %} +
+ {% include "teacher/_sidebar.html" with active="homework" %} - - {% if data.inactive_students > 0 %} -
-
{{ data.inactive_students }} Student(s) Not Participating
-

{{ data.inactive_students }} out of {{ data.total_students }} students have not started this homework.

-
- {% endif %} +
+
+ +
+ +
+
- -
-
-
-
-
{{ data.total_students }}
- Total Students +
+
+
+
Submissions
+

{{ data.homework_title }}

+

+ Due {{ data.homework_due_date|date:"F d, Y" }} · {{ data.total_sections }} section{{ data.total_sections|pluralize }}. + Review each student's progress, conversations, and answers. +

-
-
-
-
-
-
{{ data.active_students }}
- Participating + + + + {% if data.inactive_students > 0 %} +
+ {{ data.inactive_students }} Student(s) Not Participating + — {{ data.inactive_students }} out of {{ data.total_students }} students have not started this homework.
-
-
-
-
-
{{ data.inactive_students }}
- Not Started + {% endif %} + +
+
+
Total students
+
{{ data.total_students }}
+
Enrolled in this course
-
-
-
-
-
-
{{ data.total_submissions }}
- Submissions +
+
Participating
+
{{ data.active_students }}
+
Started or submitted
+
+
+
Not started
+
{{ data.inactive_students }}
+
No interaction yet
+
+
+
Submissions
+
{{ data.total_submissions }}
+
Across all students
-
-
- - -
-
- - - - +
+
+ Filter +
+ + - - -
-
- - - {% for student in data.students %} -
-
-
-
- {{ student.student_name }} - ({{ student.student_username }}) -
- {{ student.student_email }} -
-
- - {% if student.participation_status == 'no_interaction' %} - - No Interaction - - {% elif student.participation_status == 'partial' %} - - Partial Progress - - {% else %} - - Active - - {% endif %} + + - - - {% if student.has_interactions %} - {{ student.total_conversations }} conv. | {{ student.submitted_count }} submitted - {% endif %} - -
-
- -
-
-
-
- Sections Started: - {{ student.sections_started }} / {{ data.total_sections }} + + +
+
+
- {% if student.missing_sections > 0 %} - Missing Sections: - {{ student.missing_sections }} + {% for student in data.students %} +
+
+
+
+ {{ student.student_username|slice:":2"|upper }} +
+
+
+ {{ student.student_name }} · {{ student.student_username }} +
+
{{ student.student_email }}
+
+
+
+ {% if student.participation_status == 'no_interaction' %} + No Interaction + {% elif student.participation_status == 'partial' %} + Partial Progress + {% else %} + Active {% endif %} - {% if student.has_interactions %} - Last Activity: - {{ student.last_activity|date:"M d, Y H:i" }} + + {{ student.total_conversations }} conv. · {{ student.submitted_count }} submitted + {% endif %}
-
-
-
Section Progress (ordered by section number):
+ - - {% if student.widget_progress %} -
-
-
- Assessments -
+
+ + +
+ {% if student.widget_progress %} +
+
Assessments
+
+ {% for widget in student.widget_progress %} +
+
{{ widget.pre_prompt|truncatechars:32 }}
+
+ {{ widget.pre_value }} + + {% if widget.post_value is not null %}{{ widget.post_value }}{% else %}—{% endif %} {% if widget.difference is not null %} - - {{ widget.difference }} + + {{ widget.difference }} {% endif %}
+ {% endfor %}
- {% endfor %}
-
- {% endif %} + {% endif %} -
- {% for section_status in student.section_statuses %} -
-
-
- Section {{ section_status.section_order }}: {{ section_status.section_title }} - {% if section_status.is_missing %} - - Missing - - {% elif section_status.section_type == 'non_interactive' %} - - {{ section_status.answer_count }} Answer{{ section_status.answer_count|pluralize }} - - {% elif section_status.submission_count > 0 %} - - {{ section_status.submission_count }} Submitted - - {% else %} - - In Progress - +
Section progress
+
+ {% for section_status in student.section_statuses %} +
+
+
+ Section {{ section_status.section_order }}: {{ section_status.section_title }} + {% if section_status.is_missing %} + Missing + {% elif section_status.section_type == 'non_interactive' %} + {{ section_status.answer_count }} Answer{{ section_status.answer_count|pluralize }} + {% elif section_status.submission_count > 0 %} + {{ section_status.submission_count }} Submitted + {% else %} + In progress + {% endif %} +
+ {% if section_status.latest_conversation_date %} + Latest {{ section_status.latest_conversation_date|date:"M d, H:i" }} + {% elif section_status.latest_answer_date %} + Latest {{ section_status.latest_answer_date|date:"M d, H:i" }} {% endif %} -
- {% if section_status.latest_conversation_date %} - - Latest: {{ section_status.latest_conversation_date|date:"M d, Y H:i" }} - - {% elif section_status.latest_answer_date %} - - Latest: {{ section_status.latest_answer_date|date:"M d, Y H:i" }} - - {% endif %} -
+
- {% if section_status.is_missing %} -
- - {% if section_status.section_type == 'non_interactive' %} - Student has not answered this section - {% else %} - Student has not started this section - {% endif %} -
- {% elif section_status.section_type == 'non_interactive' %} -
-
-
-
Latest answer:
-

{{ section_status.latest_answer }}

-
-
- - View All ({{ section_status.answer_count }}) - + {% if section_status.is_missing %} +
+ {% if section_status.section_type == 'non_interactive' %} + Student has not answered this section. + {% else %} + Student has not started this section. + {% endif %} +
+ {% elif section_status.section_type == 'non_interactive' %} +
+
+
Latest answer
+

{{ section_status.latest_answer }}

+ + View all ({{ section_status.answer_count }}) +
-
- {% else %} -
- Conversations ({{ section_status.conversations|length }}): - {% for conversation in section_status.conversations %} -
-
-
- Conversation {{ forloop.counter }} - {% if conversation.is_deleted %} - Deleted - {% endif %} - {% if conversation.is_submitted %} - - Submitted - - {% endif %} -
-
- Created: {{ conversation.created_at|date:"M d, Y H:i" }} | - Updated: {{ conversation.updated_at|date:"M d, Y H:i" }} | - {{ conversation.message_count }} messages - {% if conversation.submission_date %} - | Submitted: {{ conversation.submission_date|date:"M d, Y H:i" }} - {% endif %} - {% if conversation.paste_event_count > 0 %} - | - {{ conversation.paste_event_count }} Paste{{ conversation.paste_event_count|pluralize }} Detected - - {% endif %} + {% else %} +
+ {% for conversation in section_status.conversations %} +
+
+
+ Conversation {{ forloop.counter }} + {% if conversation.is_deleted %} + Deleted + {% endif %} + {% if conversation.is_submitted %} + Submitted + {% endif %} + {% if conversation.paste_event_count > 0 %} + {{ conversation.paste_event_count }} paste{{ conversation.paste_event_count|pluralize }} + {% endif %} +
+
+ Created {{ conversation.created_at|date:"M d, H:i" }} · + Updated {{ conversation.updated_at|date:"M d, H:i" }} · + {{ conversation.message_count }} message{{ conversation.message_count|pluralize }} + {% if conversation.submission_date %} · Submitted {{ conversation.submission_date|date:"M d, H:i" }}{% endif %} +
+ View
- + {% endfor %}
- {% endfor %} + {% endif %}
- {% endif %} + {% endfor %}
- {% endfor %}
+ + {% empty %} +
+ No students found. There are no students in the system yet.
+ {% endfor %}
-
- {% empty %} -
-
No Students Found
-

There are no students in the system yet.

-
- {% endfor %} +
+ +{% else %} +{# Fallback for non-teacher access (kept simple; teacher_required normally gates this view) #} +
+

{{ data.homework_title }} - Submissions

+

Due: {{ data.homework_due_date|date:"F d, Y" }} | {{ data.total_sections }} sections

+ Back to homework +
+{% endif %} {% endblock %} diff --git a/src/homeworks/tests/test_dashboard_feedback_card.py b/src/homeworks/tests/test_dashboard_feedback_card.py new file mode 100644 index 0000000..07049ee --- /dev/null +++ b/src/homeworks/tests/test_dashboard_feedback_card.py @@ -0,0 +1,144 @@ +"""Tests for the student dashboard "Recent Teacher Feedback" card.""" + +from datetime import timedelta + +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone + +from accounts.models import Student, Teacher, User +from conversations.models import Conversation, TeacherFeedback +from courses.models import Course, CourseEnrollment, CourseTeacher +from homeworks.models import Homework, Section + + +class StudentDashboardFeedbackCardTests(TestCase): + def setUp(self): + self.client = Client() + + self.teacher_user = User.objects.create_user( + username="t1", email="t1@example.com", password="password123" + ) + self.teacher = Teacher.objects.create(user=self.teacher_user) + + self.student_user = User.objects.create_user( + username="s1", email="s1@example.com", password="password123" + ) + self.student = Student.objects.create(user=self.student_user) + + self.other_student_user = User.objects.create_user( + username="s2", email="s2@example.com", password="password123" + ) + self.other_student = Student.objects.create(user=self.other_student_user) + + self.course = Course.objects.create(name="C1", code="C1", description="") + CourseTeacher.objects.create( + course=self.course, teacher=self.teacher, role="owner" + ) + CourseEnrollment.objects.create(course=self.course, student=self.student) + CourseEnrollment.objects.create(course=self.course, student=self.other_student) + + self.homework = Homework.objects.create( + title="HW Title", + description="", + created_by=self.teacher, + course=self.course, + due_date=timezone.now() + timedelta(days=7), + ) + self.section = Section.objects.create( + homework=self.homework, + title="Section One", + content="content", + order=1, + ) + self.conversation = Conversation.objects.create( + user=self.student_user, section=self.section + ) + + self.list_url = reverse("homeworks:list") + + def test_no_feedback_shows_reasoning_reminder(self): + self.client.login(username="s1", password="password123") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Reasoning Reminder") + self.assertNotContains(resp, "Recent Teacher Feedback") + + def test_feedback_shows_on_dashboard_for_owning_student(self): + TeacherFeedback.objects.create( + teacher=self.teacher_user, + student=self.student_user, + section=self.section, + conversation=self.conversation, + feedback="Clarify the regression assumptions.", + feedback_type="needs_revision", + ) + self.client.login(username="s1", password="password123") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Recent Teacher Feedback") + self.assertContains(resp, "Clarify the regression assumptions.") + self.assertContains(resp, "Section One") + self.assertContains(resp, "Needs revision") + # Link to review feedback (the conversation) is present. + self.assertContains( + resp, + reverse( + "conversations:detail", kwargs={"conversation_id": self.conversation.id} + ), + ) + # Reasoning Reminder fallback should not appear. + self.assertNotContains(resp, "Reasoning Reminder") + + def test_other_students_feedback_is_not_shown(self): + # Create feedback addressed to the *other* student. + other_conv = Conversation.objects.create( + user=self.other_student_user, section=self.section + ) + TeacherFeedback.objects.create( + teacher=self.teacher_user, + student=self.other_student_user, + section=self.section, + conversation=other_conv, + feedback="Private note for s2 only.", + feedback_type="general", + ) + self.client.login(username="s1", password="password123") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 200) + # Other student's feedback must not leak. + self.assertNotContains(resp, "Private note for s2 only.") + # And without their own feedback, s1 sees the reminder. + self.assertContains(resp, "Reasoning Reminder") + + def test_latest_feedback_is_shown_when_multiple_exist(self): + # First (older) feedback. + TeacherFeedback.objects.create( + teacher=self.teacher_user, + student=self.student_user, + section=self.section, + conversation=self.conversation, + feedback="Older note.", + feedback_type="general", + ) + # Second conversation + newer feedback. + newer_section = Section.objects.create( + homework=self.homework, title="Section Two", content="x", order=2 + ) + newer_conv = Conversation.objects.create( + user=self.student_user, section=newer_section + ) + TeacherFeedback.objects.create( + teacher=self.teacher_user, + student=self.student_user, + section=newer_section, + conversation=newer_conv, + feedback="Latest reasoning is great.", + feedback_type="good_work", + ) + self.client.login(username="s1", password="password123") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Latest reasoning is great.") + self.assertContains(resp, "Section Two") + self.assertNotContains(resp, "Older note.") diff --git a/src/homeworks/views.py b/src/homeworks/views.py index 50a2ce5..2357c5c 100644 --- a/src/homeworks/views.py +++ b/src/homeworks/views.py @@ -83,6 +83,22 @@ class HomeworkListItem: is_submitted: bool = False +@dataclass +class LatestTeacherFeedbackData: + """Light DTO for the student dashboard's "Recent Teacher Feedback" card.""" + + feedback: str + feedback_type: str + feedback_type_display: str + teacher_username: str + updated_at: datetime + section_title: str + homework_title: str + homework_id: UUID | None + section_id: UUID | None + conversation_id: UUID | None + + @dataclass class HomeworkListData: """Data structure for the homework list view.""" @@ -93,6 +109,7 @@ class HomeworkListData: ] # All roles this user has: ['teacher', 'student', 'teacher_assistant'] total_count: int has_progress_data: bool + latest_teacher_feedback: LatestTeacherFeedbackData | None = None @dataclass @@ -300,12 +317,49 @@ def _get_view_data(self, user) -> HomeworkListData: ) ) + # Latest teacher feedback (students only) — visible to the logged-in + # student exclusively. Safe lookup keyed by student=user. + latest_feedback: LatestTeacherFeedbackData | None = None + if student_profile: + try: + from conversations.models import TeacherFeedback # local import + + fb = ( + TeacherFeedback.objects.filter(student=user) + .select_related( + "teacher", "section", "section__homework", "conversation" + ) + .order_by("-updated_at") + .first() + ) + if fb is not None: + latest_feedback = LatestTeacherFeedbackData( + feedback=fb.feedback, + feedback_type=fb.feedback_type, + feedback_type_display=fb.get_feedback_type_display(), + teacher_username=fb.teacher.username, + updated_at=fb.updated_at, + section_title=fb.section.title if fb.section_id else "", + homework_title=( + fb.section.homework.title + if fb.section_id and fb.section.homework_id + else "" + ), + homework_id=fb.section.homework_id if fb.section_id else None, + section_id=fb.section_id, + conversation_id=fb.conversation_id, + ) + except Exception: + # Never break the dashboard on a feedback lookup failure. + latest_feedback = None + # Create and return the view data return HomeworkListData( homeworks=homeworks, user_types=user_types, total_count=len(homeworks), has_progress_data=has_progress_data, + latest_teacher_feedback=latest_feedback, ) diff --git a/static/css/auth.css b/static/css/auth.css new file mode 100644 index 0000000..2e6ef08 --- /dev/null +++ b/static/css/auth.css @@ -0,0 +1,336 @@ +/* ============================================================ + LLTeacher — Public / Auth dark modern surfaces + Scoped under body.public-mode (set in base.html when no user + is authenticated) and only activates when the page renders + an `.auth-shell` element. + ============================================================ */ + +body.public-mode { + --a-bg: #0B0F17; + --a-surface: #151B26; + --a-surface-2: #1A2233; + --a-surface-3: #212B3D; + --a-border: #232E42; + --a-border-2: #2E3A52; + + --a-text: #F9FAFB; + --a-text-dim: #A1A1AA; + --a-text-mute: #71717A; + + --a-blue: #3B82F6; + --a-blue-2: #60A5FA; + --a-blue-soft: rgba(59,130,246,0.14); + + --a-purple: #8B5CF6; + + --a-ease: cubic-bezier(0.2, 0.7, 0.2, 1); + + background-color: var(--a-bg); + color: var(--a-text); + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* Hide the legacy navbar + footer on auth shell pages */ +body.public-mode:has(.auth-shell) > nav.navbar, +body.public-mode:has(.auth-shell) > footer.bg-light { + display: none !important; +} +body.public-mode:has(.auth-shell) > main.container, +body.public-mode:has(.auth-shell) > .container.mt-3 { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; +} + +/* ============================================================ + Auth shell layout + ============================================================ */ +body.public-mode .auth-shell { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; + background: + radial-gradient(circle at 20% 0%, rgba(59,130,246,0.10), transparent 50%), + radial-gradient(circle at 80% 100%, rgba(139,92,246,0.10), transparent 55%), + var(--a-bg); +} + +body.public-mode .auth-topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 22px 32px; +} +body.public-mode .auth-topbar .brand { + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; + color: var(--a-text); +} +body.public-mode .auth-topbar .brand-mark { + width: 34px; + height: 34px; + border-radius: 9px; + background: linear-gradient(135deg, var(--a-blue), var(--a-purple)); + display: grid; + place-items: center; + color: white; + font-weight: 700; + font-size: 13px; + letter-spacing: -0.02em; +} +body.public-mode .auth-topbar .brand-name { + font-weight: 600; + font-size: 16px; + letter-spacing: -0.01em; +} +body.public-mode .auth-topbar .brand-sub { + display: block; + font-size: 11px; + color: var(--a-text-mute); + margin-top: 1px; +} +body.public-mode .auth-topbar-links { + display: flex; + gap: 8px; + align-items: center; +} +body.public-mode .auth-topbar-links a { + color: var(--a-text-dim); + font-size: 13.5px; + font-weight: 500; + padding: 8px 14px; + border-radius: 8px; + text-decoration: none; + transition: background 150ms var(--a-ease), color 150ms var(--a-ease); +} +body.public-mode .auth-topbar-links a:hover { + background: var(--a-surface-2); + color: var(--a-text); +} +body.public-mode .auth-topbar-links a.primary { + background: var(--a-blue); + color: white; +} +body.public-mode .auth-topbar-links a.primary:hover { + background: var(--a-blue-2); + color: white; + box-shadow: 0 4px 16px -6px rgba(59,130,246,0.6); +} + +body.public-mode .auth-main { + display: grid; + place-items: center; + padding: 32px 24px; +} +body.public-mode .auth-content { + width: 100%; + max-width: 920px; + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); + gap: 40px; + align-items: center; +} +@media (max-width: 880px) { + body.public-mode .auth-content { + grid-template-columns: 1fr; + max-width: 460px; + } + body.public-mode .auth-hero { display: none; } +} + +body.public-mode .auth-hero { + color: var(--a-text); +} +body.public-mode .auth-hero .eyebrow { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--a-text-mute); + margin-bottom: 14px; +} +body.public-mode .auth-hero h1 { + font-size: 38px; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1.1; + margin: 0 0 14px; + color: var(--a-text); +} +body.public-mode .auth-hero p { + font-size: 15px; + color: var(--a-text-dim); + line-height: 1.6; + margin: 0 0 22px; + max-width: 460px; +} +body.public-mode .auth-feature-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; +} +body.public-mode .auth-feature-list li { + display: flex; + align-items: flex-start; + gap: 12px; + color: var(--a-text-dim); + font-size: 13.5px; + line-height: 1.55; +} +body.public-mode .auth-feature-list .feature-icon { + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--a-blue-soft); + color: var(--a-blue-2); + display: grid; + place-items: center; + flex-shrink: 0; +} + +body.public-mode .auth-card { + background: var(--a-surface); + border: 1px solid var(--a-border); + border-radius: 18px; + padding: 32px; + box-shadow: 0 18px 50px -28px rgba(0,0,0,0.6); + animation: authCardIn 420ms var(--a-ease) both; +} +body.public-mode .auth-card h2 { + font-size: 22px; + font-weight: 600; + letter-spacing: -0.015em; + margin: 0 0 4px; + color: var(--a-text); +} +body.public-mode .auth-card .auth-tag { + font-size: 13.5px; + color: var(--a-text-dim); + margin: 0 0 22px; +} + +body.public-mode .auth-card .field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 14px; +} +body.public-mode .auth-card .field label, +body.public-mode .auth-card .form-label { + font-size: 12px; + color: var(--a-text-dim); + letter-spacing: 0.04em; + text-transform: uppercase; +} +body.public-mode .auth-card input[type="text"], +body.public-mode .auth-card input[type="email"], +body.public-mode .auth-card input[type="password"], +body.public-mode .auth-card .form-control { + width: 100%; + background: var(--a-surface-2); + border: 1px solid var(--a-border); + border-radius: 10px; + padding: 11px 13px; + color: var(--a-text); + font-size: 14px; + transition: border-color 150ms var(--a-ease), background 150ms var(--a-ease); +} +body.public-mode .auth-card input:focus, +body.public-mode .auth-card .form-control:focus { + outline: none; + border-color: var(--a-blue); + background: var(--a-surface-3); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + color: var(--a-text); +} +body.public-mode .auth-card input::placeholder { color: var(--a-text-mute); } + +body.public-mode .auth-card .form-text { + color: var(--a-text-mute); + font-size: 11.5px; + margin-top: 4px; +} + +body.public-mode .auth-card .alert.alert-danger { + background: rgba(239,68,68,0.14); + color: #fca5a5; + border: 1px solid rgba(239,68,68,0.3); + border-radius: 10px; + font-size: 13px; + padding: 10px 12px; + margin-bottom: 14px; +} +body.public-mode .auth-card .alert.alert-danger p { margin: 0; } +body.public-mode .auth-card .invalid-feedback, +body.public-mode .auth-card .invalid-feedback.d-block { + color: #fca5a5; + font-size: 12px; + margin-top: 4px; +} + +body.public-mode .auth-card .btn-primary, +body.public-mode .auth-card button[type="submit"] { + width: 100%; + background: var(--a-blue); + border: 1px solid var(--a-blue); + color: white; + font-size: 14.5px; + font-weight: 500; + padding: 11px 16px; + border-radius: 10px; + cursor: pointer; + margin-top: 6px; + transition: background 150ms var(--a-ease), box-shadow 150ms var(--a-ease), transform 100ms var(--a-ease); +} +body.public-mode .auth-card .btn-primary:hover, +body.public-mode .auth-card button[type="submit"]:hover { + background: var(--a-blue-2); + border-color: var(--a-blue-2); + box-shadow: 0 4px 16px -6px rgba(59,130,246,0.6); + transform: translateY(-1px); +} + +body.public-mode .auth-card .auth-foot { + text-align: center; + color: var(--a-text-mute); + font-size: 13px; + margin-top: 18px; + padding-top: 18px; + border-top: 1px solid var(--a-border); +} +body.public-mode .auth-card .auth-foot a { + color: var(--a-blue-2); + text-decoration: none; + font-weight: 500; +} +body.public-mode .auth-card .auth-foot a:hover { color: var(--a-blue); } + +body.public-mode .auth-card .forgot-link { + text-align: center; + margin-top: 10px; +} +body.public-mode .auth-card .forgot-link a { + color: var(--a-text-dim); + font-size: 12.5px; + text-decoration: none; +} +body.public-mode .auth-card .forgot-link a:hover { color: var(--a-text); } + +body.public-mode .auth-footer { + padding: 18px 32px; + color: var(--a-text-mute); + font-size: 11.5px; + text-align: center; +} +body.public-mode .auth-footer a { color: var(--a-blue-2); text-decoration: none; } +body.public-mode .auth-footer a:hover { color: var(--a-blue); } + +@keyframes authCardIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } +} diff --git a/static/css/student.css b/static/css/student.css new file mode 100644 index 0000000..9da0f01 --- /dev/null +++ b/static/css/student.css @@ -0,0 +1,1391 @@ +/* ============================================================ + LLTeacher — Student dark modern app shell + All rules scoped under body.student-mode so they never leak + into teacher / TA / unauthenticated pages. + ============================================================ */ + +body.student-mode { + --s-bg: #0B0F17; + --s-sidebar: #0F1421; + --s-surface: #151B26; + --s-surface-2: #1A2233; + --s-surface-3: #212B3D; + --s-border: #232E42; + --s-border-2: #2E3A52; + + --s-text: #F9FAFB; + --s-text-dim: #A1A1AA; + --s-text-mute: #71717A; + + --s-blue: #3B82F6; + --s-blue-2: #60A5FA; + --s-blue-soft: rgba(59, 130, 246, 0.14); + + --s-green: #22C55E; + --s-green-soft: rgba(34, 197, 94, 0.14); + + --s-amber: #F59E0B; + --s-amber-soft: rgba(245, 158, 11, 0.14); + + --s-red: #EF4444; + --s-red-soft: rgba(239, 68, 68, 0.14); + + --s-purple: #8B5CF6; + --s-purple-soft: rgba(139, 92, 246, 0.14); + + --s-ease: cubic-bezier(0.2, 0.7, 0.2, 1); + + background-color: var(--s-bg); + color: var(--s-text); + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* ============================================================ + Hide legacy Bootstrap navbar + footer for student pages that + use the new shell. Uses :has() — modern Chrome/Safari/Firefox. + Pages without .student-shell (e.g. homepage, profile) keep the + legacy navbar so students still have navigation there. + ============================================================ */ +body.student-mode:has(.student-shell) > nav.navbar, +body.student-mode:has(.student-shell) > footer.bg-light { + display: none !important; +} +body.student-mode:has(.student-shell) > .container.mt-3 .alert { + border: 0 !important; + border-bottom: 1px solid var(--s-border) !important; + border-radius: 0 !important; + margin: 0 !important; + padding: 12px 32px !important; + background: var(--s-surface-2); + color: var(--s-text); + font-size: 13.5px; +} +body.student-mode:has(.student-shell) > .container.mt-3 .alert.alert-success { + background: var(--s-green-soft, rgba(34,197,94,0.10)); + color: var(--s-green); +} +body.student-mode:has(.student-shell) > .container.mt-3 .alert.alert-warning { + background: var(--s-amber-soft, rgba(245,158,11,0.10)); + color: var(--s-amber); +} +body.student-mode:has(.student-shell) > .container.mt-3 .alert.alert-danger, +body.student-mode:has(.student-shell) > .container.mt-3 .alert.alert-error { + background: var(--s-red-soft, rgba(239,68,68,0.10)); + color: var(--s-red); +} +body.student-mode:has(.student-shell) > .container.mt-3 .alert.alert-info { + background: var(--s-blue-soft, rgba(59,130,246,0.10)); + color: var(--s-blue); +} +body.student-mode:has(.student-shell) > .container.mt-3 .btn-close { + filter: invert(1) opacity(0.7); +} +body.student-mode:has(.student-shell) > main.container, +body.student-mode:has(.student-shell) > .container.mt-3 { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; +} + +/* Fallback for browsers without :has() — still show student-shell, + navbar will sit on top but doesn't break the page. */ + +/* ============================================================ + App shell layout + ============================================================ */ +body.student-mode .student-shell { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + min-height: 100vh; + background: var(--s-bg); +} + +/* ============================================================ + Sidebar + ============================================================ */ +body.student-mode .student-sidebar { + background: var(--s-sidebar); + border-right: 1px solid var(--s-border); + padding: 22px 14px 18px; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; +} +body.student-mode .student-sidebar .brand { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 8px 22px; + text-decoration: none; + color: var(--s-text); +} +body.student-mode .student-sidebar .brand-mark { + width: 34px; + height: 34px; + border-radius: 9px; + background: linear-gradient(135deg, var(--s-blue), var(--s-purple)); + display: grid; + place-items: center; + color: white; + font-weight: 700; + font-size: 13px; + letter-spacing: -0.02em; +} +body.student-mode .student-sidebar .brand-name { + font-weight: 600; + font-size: 15px; + letter-spacing: -0.01em; + display: block; +} +body.student-mode .student-sidebar .brand-sub { + font-size: 11px; + color: var(--s-text-mute); + display: block; + margin-top: 2px; +} + +body.student-mode .student-sidebar .nav-section { + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--s-text-mute); + padding: 14px 10px 6px; +} +body.student-mode .student-nav { + display: flex; + flex-direction: column; + gap: 2px; +} +body.student-mode .student-nav .nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 9px 10px; + border-radius: 9px; + color: var(--s-text-dim); + font-size: 14px; + text-decoration: none; + transition: background 150ms var(--s-ease), color 150ms var(--s-ease); +} +body.student-mode .student-nav .nav-item:hover { + background: var(--s-surface-2); + color: var(--s-text); +} +body.student-mode .student-nav .nav-item.active { + background: var(--s-surface-3); + color: var(--s-text); + box-shadow: inset 0 0 0 1px var(--s-border); +} +body.student-mode .student-nav .nav-item svg { + width: 16px; + height: 16px; + flex-shrink: 0; + opacity: 0.9; +} + +body.student-mode .sidebar-foot { + margin-top: auto; + padding: 12px 8px 4px; + border-top: 1px solid var(--s-border); + display: flex; + align-items: center; + gap: 10px; +} +body.student-mode .sidebar-foot .avatar { + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, #2a3142, #444c63); + display: grid; + place-items: center; + font-size: 12px; + font-weight: 600; + color: var(--s-text); + flex-shrink: 0; +} +body.student-mode .sidebar-foot .who { min-width: 0; flex: 1; } +body.student-mode .sidebar-foot .who-name { + font-size: 13px; + font-weight: 500; + color: var(--s-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +body.student-mode .sidebar-foot .who-role { + font-size: 11px; + color: var(--s-text-mute); +} +body.student-mode .sidebar-foot .sidebar-logout { + margin-left: auto; + width: 30px; + height: 30px; + border-radius: 8px; + color: var(--s-text-mute); + display: grid; + place-items: center; + transition: background 150ms var(--s-ease), color 150ms var(--s-ease); +} +body.student-mode .sidebar-foot .sidebar-logout:hover { + background: var(--s-surface-2); + color: var(--s-text); +} + +/* ============================================================ + Main column + ============================================================ */ +body.student-mode .student-main { + display: flex; + flex-direction: column; + min-width: 0; +} + +body.student-mode .student-topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + gap: 16px; + padding: 16px 32px; + border-bottom: 1px solid var(--s-border); + background: rgba(11, 15, 23, 0.88); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + min-height: 64px; +} +body.student-mode .topbar-crumbs { + font-size: 13px; + color: var(--s-text-mute); + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} +body.student-mode .topbar-crumbs a { + color: var(--s-text-dim); + text-decoration: none; +} +body.student-mode .topbar-crumbs a:hover { color: var(--s-text); } +body.student-mode .topbar-crumbs .crumb-now { color: var(--s-text); } +body.student-mode .topbar-crumbs .sep { color: var(--s-text-mute); } + +body.student-mode .topbar-actions { + display: flex; + align-items: center; + gap: 10px; +} +body.student-mode .topbar-avatar { + width: 30px; + height: 30px; + border-radius: 8px; + background: linear-gradient(135deg, #2a3142, #444c63); + display: grid; + place-items: center; + font-size: 11px; + font-weight: 600; + color: var(--s-text); +} + +body.student-mode .student-content { + padding: 32px; + max-width: 1280px; + width: 100%; + margin: 0 auto; +} +@media (max-width: 768px) { + body.student-mode .student-content { padding: 20px 18px; } +} + +/* ============================================================ + Page header + ============================================================ */ +body.student-mode .page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 28px; + flex-wrap: wrap; + animation: studentFadeUp 380ms var(--s-ease) both; +} +body.student-mode .page-eyebrow { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--s-text-mute); + margin-bottom: 8px; +} +body.student-mode .page-title { + font-size: 28px; + font-weight: 500; + letter-spacing: -0.02em; + margin: 0 0 4px; + line-height: 1.15; + color: var(--s-text); +} +body.student-mode .page-sub { + color: var(--s-text-dim); + font-size: 14.5px; + margin: 0; + max-width: 640px; +} + +/* ============================================================ + Generic student cards + ============================================================ */ +body.student-mode .student-card { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 14px; + padding: 22px 24px; + color: var(--s-text); + box-shadow: 0 1px 2px rgba(0,0,0,0.3); + transition: border-color 200ms var(--s-ease), transform 200ms var(--s-ease); +} +body.student-mode .student-card .card-eyebrow { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--s-text-mute); + margin-bottom: 8px; +} +body.student-mode .student-card .card-title { + font-size: 17px; + font-weight: 500; + letter-spacing: -0.01em; + margin: 0 0 6px; + color: var(--s-text); +} + +/* Two-column dashboard grid */ +body.student-mode .student-grid { + display: grid; + grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr); + gap: 24px; + align-items: start; +} +@media (max-width: 1100px) { + body.student-mode .student-grid { grid-template-columns: 1fr; } +} +body.student-mode .student-grid-main, +body.student-mode .student-grid-aside { + display: flex; + flex-direction: column; + gap: 22px; + min-width: 0; +} +body.student-mode .stagger > * { + opacity: 0; + animation: studentCardIn 420ms var(--s-ease) both; +} +body.student-mode .stagger > *:nth-child(1) { animation-delay: 50ms; } +body.student-mode .stagger > *:nth-child(2) { animation-delay: 110ms; } +body.student-mode .stagger > *:nth-child(3) { animation-delay: 170ms; } +body.student-mode .stagger > *:nth-child(4) { animation-delay: 230ms; } +body.student-mode .stagger > *:nth-child(5) { animation-delay: 290ms; } +body.student-mode .stagger > *:nth-child(6) { animation-delay: 350ms; } + +/* ============================================================ + Hero card (Continue Working) + ============================================================ */ +body.student-mode .student-hero-card { + background: + linear-gradient(135deg, rgba(59,130,246,0.10), transparent 65%), + var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 18px; + padding: 28px 32px; + position: relative; + overflow: hidden; + box-shadow: + 0 1px 2px rgba(0,0,0,0.3), + 0 18px 50px -28px rgba(59,130,246,0.42); + animation: studentSlideIn 440ms var(--s-ease) both; +} +body.student-mode .student-hero-card::after { + content: ""; + position: absolute; + top: -80px; + right: -80px; + width: 260px; + height: 260px; + background: radial-gradient(circle, rgba(139,92,246,0.18), transparent 70%); + pointer-events: none; +} +body.student-mode .student-hero-card > * { position: relative; } +/* Render only the first hero card if the template emits several. */ +body.student-mode .student-hero-card ~ .student-hero-card { display: none !important; } +body.student-mode .hero-eyebrow { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} +body.student-mode .hero-eyebrow .eyebrow-label { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--s-text-mute); +} +body.student-mode .hero-title { + font-size: 24px; + font-weight: 500; + letter-spacing: -0.015em; + line-height: 1.2; + margin: 0 0 6px; + color: var(--s-text); +} +body.student-mode .hero-meta { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + color: var(--s-text-dim); + font-size: 13px; + margin: 6px 0 18px; +} +body.student-mode .hero-meta .meta-dot { color: var(--s-text-mute); } +body.student-mode .hero-meta .meta-frac { color: var(--s-text); font-weight: 500; } +body.student-mode .hero-meta i { margin-right: 4px; opacity: 0.85; } + +body.student-mode .hero-next { + background: var(--s-surface-2); + border: 1px solid var(--s-border); + border-radius: 10px; + padding: 12px 14px; + font-size: 13.5px; + color: var(--s-text-dim); + margin: 0 0 20px; + line-height: 1.55; +} +body.student-mode .hero-next strong { color: var(--s-text); font-weight: 500; } + +body.student-mode .hero-actions { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} + +/* ============================================================ + Buttons + ============================================================ */ +body.student-mode .btn-s { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 18px; + border-radius: 9px; + background: var(--s-surface-2); + border: 1px solid var(--s-border-2); + color: var(--s-text); + font-size: 13.5px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: background 150ms var(--s-ease), border-color 150ms var(--s-ease), + transform 100ms var(--s-ease), box-shadow 150ms var(--s-ease); +} +body.student-mode .btn-s:hover { + background: var(--s-surface-3); + border-color: #3a445a; + transform: translateY(-1px); +} +body.student-mode .btn-s-primary { + background: var(--s-blue); + border-color: var(--s-blue); + color: white; +} +body.student-mode .btn-s-primary:hover { + background: var(--s-blue-2); + border-color: var(--s-blue-2); + color: white; + box-shadow: 0 4px 16px -6px rgba(59,130,246,0.6); +} +body.student-mode .btn-s-purple { + background: var(--s-purple-soft); + border-color: rgba(139,92,246,0.4); + color: var(--s-purple); +} +body.student-mode .btn-s-purple:hover { + background: rgba(139,92,246,0.22); + color: var(--s-purple); + border-color: rgba(139,92,246,0.6); +} +body.student-mode .btn-s-ghost { + background: transparent; + border-color: transparent; + color: var(--s-text-dim); +} +body.student-mode .btn-s-ghost:hover { + background: var(--s-surface-2); + color: var(--s-text); + border-color: transparent; +} +body.student-mode .btn-s[disabled] { opacity: 0.5; cursor: not-allowed; transform: none; } +body.student-mode .btn-s-sm { padding: 6px 12px; font-size: 12.5px; } + +/* ============================================================ + Status badges + ============================================================ */ +body.student-mode .student-status { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 500; + padding: 3px 9px; + border-radius: 999px; + border: 1px solid var(--s-border); + background: var(--s-surface-2); + color: var(--s-text-dim); + white-space: nowrap; +} +body.student-mode .student-status .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} +body.student-mode .student-status.green { + color: var(--s-green); + background: var(--s-green-soft); + border-color: rgba(34,197,94,0.3); +} +body.student-mode .student-status.amber { + color: var(--s-amber); + background: var(--s-amber-soft); + border-color: rgba(245,158,11,0.3); +} +body.student-mode .student-status.red { + color: var(--s-red); + background: var(--s-red-soft); + border-color: rgba(239,68,68,0.3); +} +body.student-mode .student-status.blue { + color: var(--s-blue-2); + background: var(--s-blue-soft); + border-color: rgba(59,130,246,0.3); +} +body.student-mode .student-status.purple { + color: var(--s-purple); + background: var(--s-purple-soft); + border-color: rgba(139,92,246,0.3); +} +body.student-mode .student-status.mute { + color: var(--s-text-mute); + background: var(--s-surface-2); +} + +/* ============================================================ + Progress bars + ============================================================ */ +body.student-mode .student-progress { + height: 8px; + background: var(--s-surface-3); + border-radius: 999px; + overflow: hidden; + display: flex; +} +body.student-mode .student-progress.tall { height: 10px; } +body.student-mode .student-progress .bar { + height: 100%; + background: linear-gradient(90deg, var(--s-blue), var(--s-blue-2)); + transition: width 700ms var(--s-ease); +} +body.student-mode .student-progress .bar.green { + background: linear-gradient(90deg, var(--s-green), #34d399); +} +body.student-mode .student-progress .bar.amber { + background: linear-gradient(90deg, var(--s-amber), #FBBF24); +} + +/* ============================================================ + Due-soon list (homework list aside) + ============================================================ */ +body.student-mode .due-list { + list-style: none; + padding: 0; + margin: 0; +} +body.student-mode .due-row { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 4px; + border-bottom: 1px solid var(--s-border); + transition: background 150ms var(--s-ease), padding 150ms var(--s-ease); +} +body.student-mode .due-row:last-child { border-bottom: none; } +body.student-mode .due-row:hover { + background: var(--s-surface-2); + padding-left: 10px; + padding-right: 10px; +} +body.student-mode .due-date { + width: 46px; + text-align: center; + flex-shrink: 0; + font-family: ui-monospace, "SF Mono", Menlo, monospace; +} +body.student-mode .due-month { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--s-text-mute); +} +body.student-mode .due-day { + font-size: 20px; + font-weight: 500; + line-height: 1.1; + color: var(--s-text); +} +body.student-mode .due-main { flex: 1; min-width: 0; } +body.student-mode .due-name { font-size: 14.5px; color: var(--s-text); } +body.student-mode .due-sub { font-size: 12.5px; color: var(--s-text-mute); margin-top: 2px; } +body.student-mode .due-row-wrap { + padding: 14px 4px; + border-bottom: 1px solid var(--s-border); +} +body.student-mode .due-row-wrap:last-child { border-bottom: none; } +body.student-mode .due-row-wrap .due-row { + padding: 0; + border-bottom: none; +} +body.student-mode .due-row-wrap .due-row:hover { padding: 0; background: transparent; } +body.student-mode .due-sections { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 12px; + padding-left: 62px; +} +@media (max-width: 640px) { + body.student-mode .due-sections { padding-left: 0; } +} +body.student-mode .section-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 10px 5px 6px; + border-radius: 999px; + background: var(--s-surface-2); + border: 1px solid var(--s-border); + font-size: 12px; + color: var(--s-text-dim); + text-decoration: none; + transition: background 150ms var(--s-ease), color 150ms var(--s-ease), border-color 150ms var(--s-ease); +} +body.student-mode .section-pill:hover { + background: var(--s-surface-3); + color: var(--s-text); + border-color: var(--s-border-2); +} +body.student-mode .section-pill-num { + display: grid; + place-items: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + background: var(--s-surface-3); + color: var(--s-text-dim); + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 10px; +} +body.student-mode .section-pill-status { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--s-text-mute); +} +body.student-mode .section-pill-status.green { background: var(--s-green); } +body.student-mode .section-pill-status.amber { background: var(--s-amber); } +body.student-mode .section-pill-status.red { background: var(--s-red); } +body.student-mode .section-pill-status.mute { background: var(--s-text-mute); opacity: 0.6; } + +/* ============================================================ + Tutor feedback card (aside) + ============================================================ */ +body.student-mode .tutor-feedback-card { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-left: 2px solid var(--s-purple); + border-radius: 12px; + padding: 20px 22px; +} +body.student-mode .tutor-feedback-card .tfc-label { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--s-text-mute); + margin-bottom: 10px; +} +body.student-mode .tutor-feedback-card .tfc-quote { + font-size: 14px; + line-height: 1.6; + margin: 0 0 14px; + color: var(--s-text); +} +body.student-mode .tutor-feedback-card .tfc-source { + font-size: 11px; + color: var(--s-text-mute); + font-family: ui-monospace, "SF Mono", Menlo, monospace; +} + +/* ============================================================ + Section list (assignment detail) + ============================================================ */ +body.student-mode .section-list { + display: flex; + flex-direction: column; + gap: 10px; +} +body.student-mode .section-row { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 12px; + color: var(--s-text); + text-decoration: none; + transition: border-color 180ms var(--s-ease), transform 150ms var(--s-ease); +} +body.student-mode .section-row:hover { + border-color: var(--s-border-2); + transform: translateY(-1px); + color: var(--s-text); +} +body.student-mode .section-row .leading { + width: 32px; + height: 32px; + border-radius: 8px; + background: var(--s-surface-2); + display: grid; + place-items: center; + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 12px; + color: var(--s-text-dim); + flex-shrink: 0; +} +body.student-mode .section-row .name { + flex: 1; + min-width: 0; + font-size: 14.5px; +} +body.student-mode .section-row .name .small { + font-size: 12px; + color: var(--s-text-mute); + margin-top: 2px; +} + +/* ============================================================ + Section workspace (section_detail student branch) + ============================================================ */ +body.student-mode .student-workspace { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 16px; + padding: 24px 26px; + margin-bottom: 24px; +} +body.student-mode .student-workspace .ws-eyebrow { + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--s-text-mute); + margin-bottom: 4px; +} +body.student-mode .student-workspace .ws-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 6px; + color: var(--s-text); +} +body.student-mode .student-workspace .ws-sub { + font-size: 13.5px; + color: var(--s-text-dim); + margin-bottom: 16px; + line-height: 1.55; +} +body.student-mode .student-workspace .reasoning-textarea { + width: 100%; + min-height: 130px; + background: var(--s-surface-2); + border: 1px solid var(--s-border); + color: var(--s-text); + border-radius: 10px; + padding: 12px 14px; + font-family: inherit; + font-size: 14px; + line-height: 1.55; + resize: vertical; +} +body.student-mode .student-workspace .reasoning-textarea:focus { + outline: none; + border-color: var(--s-blue); + background: var(--s-surface-3); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); +} +body.student-mode .student-workspace .reasoning-textarea::placeholder { color: var(--s-text-mute); } + +body.student-mode .reasoning-meter { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; + font-size: 12px; + color: var(--s-text-mute); +} +body.student-mode .reasoning-meter .meter-bar { + flex: 1; + height: 4px; + background: var(--s-surface-3); + border-radius: 999px; + overflow: hidden; +} +body.student-mode .reasoning-meter .meter-fill { + height: 100%; + width: 0%; + background: var(--s-amber); + border-radius: 999px; + transition: width 300ms var(--s-ease), background 300ms var(--s-ease); +} +body.student-mode .reasoning-keywords { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 10px; +} +body.student-mode .reasoning-keyword { + font-size: 11px; + padding: 3px 9px; + border-radius: 999px; + background: var(--s-surface-3); + border: 1px solid var(--s-border); + color: var(--s-text-mute); + transition: all 200ms var(--s-ease); +} +body.student-mode .reasoning-keyword.hit { + background: var(--s-green-soft); + border-color: rgba(34,197,94,0.3); + color: var(--s-green); +} +body.student-mode .reasoning-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 18px; +} +body.student-mode .reasoning-feedback { + margin-top: 14px; + padding: 12px 14px; + border-radius: 10px; + font-size: 13.5px; + line-height: 1.55; + display: flex; + gap: 12px; + align-items: flex-start; + animation: studentPopIn 240ms var(--s-ease) both; +} +body.student-mode .reasoning-feedback.correct { + background: var(--s-green-soft); + border: 1px solid rgba(34,197,94,0.3); + color: var(--s-green); +} +body.student-mode .reasoning-feedback.partial { + background: var(--s-amber-soft); + border: 1px solid rgba(245,158,11,0.3); + color: var(--s-amber); +} +body.student-mode .reasoning-feedback.incorrect { + background: var(--s-red-soft); + border: 1px solid rgba(239,68,68,0.3); + color: var(--s-red); +} +body.student-mode .reasoning-feedback .fb-title { + font-weight: 600; + margin-bottom: 2px; +} +body.student-mode .reasoning-feedback .fb-body { + color: var(--s-text); + flex: 1; +} + +/* Tutor side panel */ +body.student-mode .workspace-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 22px; + align-items: start; +} +@media (max-width: 1200px) { + body.student-mode .workspace-grid { grid-template-columns: 1fr; } +} +body.student-mode .student-tutor-panel { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 14px; + padding: 18px 20px; + position: sticky; + top: 88px; +} +body.student-mode .student-tutor-panel .tp-head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; + padding-bottom: 14px; + border-bottom: 1px solid var(--s-border); +} +body.student-mode .student-tutor-panel .tp-avatar { + width: 34px; + height: 34px; + border-radius: 9px; + background: linear-gradient(135deg, var(--s-purple), var(--s-blue)); + display: grid; + place-items: center; + color: white; + font-weight: 700; + font-size: 12px; + flex-shrink: 0; +} +body.student-mode .student-tutor-panel .tp-title { + font-size: 14.5px; + font-weight: 600; + color: var(--s-text); +} +body.student-mode .student-tutor-panel .tp-sub { + font-size: 11.5px; + color: var(--s-text-mute); +} +body.student-mode .student-tutor-panel .tp-quick { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + margin-bottom: 14px; +} +body.student-mode .student-tutor-panel .tp-quick button { + text-align: left; + padding: 8px 10px; + border-radius: 8px; + background: var(--s-surface-2); + border: 1px solid var(--s-border); + color: var(--s-text-dim); + font-size: 11.5px; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: background 150ms var(--s-ease), color 150ms var(--s-ease); +} +body.student-mode .student-tutor-panel .tp-quick button:hover { + background: var(--s-surface-3); + color: var(--s-text); +} +body.student-mode .student-tutor-panel .tp-quick button svg { + width: 12px; + height: 12px; + opacity: 0.8; + flex-shrink: 0; +} +body.student-mode .student-tutor-panel .tp-response { + background: var(--s-purple-soft); + border: 1px solid rgba(139,92,246,0.25); + border-radius: 10px; + padding: 12px 14px; + font-size: 13px; + line-height: 1.55; + color: var(--s-text); + margin-bottom: 12px; + animation: studentPopIn 280ms var(--s-ease) both; +} +body.student-mode .student-tutor-panel .tp-response .tp-tag { + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--s-purple); + margin-bottom: 4px; + font-weight: 600; +} +body.student-mode .student-tutor-panel .tp-link { + display: block; + text-align: center; + padding: 10px 14px; + border-radius: 9px; + background: var(--s-purple-soft); + border: 1px solid rgba(139,92,246,0.4); + color: var(--s-purple); + font-size: 13px; + font-weight: 500; + text-decoration: none; + transition: background 150ms var(--s-ease); +} +body.student-mode .student-tutor-panel .tp-link:hover { + background: rgba(139,92,246,0.22); + color: var(--s-purple); +} + +/* ============================================================ + Submission / answer Bootstrap forms inside student-content + (used by section_detail submission card) + ============================================================ */ +body.student-mode .student-content .card { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 14px; + color: var(--s-text); + box-shadow: 0 1px 2px rgba(0,0,0,0.3); +} +body.student-mode .student-content .card-header, +body.student-mode .student-content .card-header.bg-light { + background: var(--s-surface-2) !important; + border-bottom: 1px solid var(--s-border); + color: var(--s-text); +} +body.student-mode .student-content .form-control, +body.student-mode .student-content textarea.form-control { + background: var(--s-surface-2); + color: var(--s-text); + border: 1px solid var(--s-border); + border-radius: 10px; +} +body.student-mode .student-content .form-control:focus, +body.student-mode .student-content textarea.form-control:focus { + background: var(--s-surface-3); + border-color: var(--s-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + color: var(--s-text); +} +body.student-mode .student-content .alert.alert-info { + background: var(--s-blue-soft); + border-color: rgba(59,130,246,0.3); + color: var(--s-blue-2); +} +body.student-mode .student-content .alert.alert-success { + background: var(--s-green-soft); + border-color: rgba(34,197,94,0.3); + color: var(--s-green); +} +body.student-mode .student-content .alert.alert-warning { + background: var(--s-amber-soft); + border-color: rgba(245,158,11,0.3); + color: var(--s-amber); +} +body.student-mode .student-content .btn-primary { + background: var(--s-blue); + border-color: var(--s-blue); + border-radius: 9px; +} +body.student-mode .student-content .btn-primary:hover { + background: var(--s-blue-2); + border-color: var(--s-blue-2); +} +body.student-mode .student-content .btn-success { + background: var(--s-green); + border-color: var(--s-green); + border-radius: 9px; +} +body.student-mode .student-content .btn-outline-secondary { + color: var(--s-text-dim); + border-color: var(--s-border-2); + border-radius: 9px; +} +body.student-mode .student-content .btn-outline-secondary:hover { + background: var(--s-surface-3); + color: var(--s-text); + border-color: var(--s-border-2); +} +body.student-mode .student-content .breadcrumb { + background: transparent; + padding: 0; +} +body.student-mode .student-content .breadcrumb-item a { color: var(--s-text-dim); } +body.student-mode .student-content .breadcrumb-item.active { color: var(--s-text-mute); } +body.student-mode .student-content .text-muted { color: var(--s-text-mute) !important; } + +/* Course cards (courses/list student branch) */ +body.student-mode .course-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; +} +body.student-mode .course-card { + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 16px; + overflow: hidden; + display: flex; + flex-direction: column; + text-decoration: none; + color: var(--s-text); + transition: transform 200ms var(--s-ease), border-color 200ms var(--s-ease), + box-shadow 200ms var(--s-ease); + animation: studentCardIn 420ms var(--s-ease) both; +} +body.student-mode .course-card:hover { + transform: translateY(-2px); + border-color: var(--s-border-2); + box-shadow: 0 18px 42px -26px rgba(0,0,0,0.5); + color: var(--s-text); +} +body.student-mode .course-cover { + height: 88px; + background: + linear-gradient(135deg, rgba(59,130,246,0.55), rgba(139,92,246,0.40)), + repeating-linear-gradient(45deg, rgba(255,255,255,0.04) 0 8px, transparent 8px 16px); +} +body.student-mode .course-cover.violet { + background: + linear-gradient(135deg, rgba(139,92,246,0.55), rgba(239,68,68,0.30)), + repeating-linear-gradient(45deg, rgba(255,255,255,0.04) 0 8px, transparent 8px 16px); +} +body.student-mode .course-cover.teal { + background: + linear-gradient(135deg, rgba(34,197,94,0.50), rgba(59,130,246,0.40)), + repeating-linear-gradient(45deg, rgba(255,255,255,0.04) 0 8px, transparent 8px 16px); +} +body.student-mode .course-body { + padding: 18px 20px; + display: flex; + flex-direction: column; + gap: 10px; + flex: 1; +} +body.student-mode .course-body h3 { + margin: 0; + font-size: 17px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--s-text); +} +body.student-mode .course-body .course-sub { + color: var(--s-text-dim); + font-size: 13px; +} +body.student-mode .course-body .course-meta { + font-size: 12.5px; + color: var(--s-text-mute); + display: flex; + flex-direction: column; + gap: 4px; + margin: 4px 0 6px; +} +body.student-mode .course-foot { + margin-top: auto; +} + +/* ============================================================ + Animations + ============================================================ */ +@keyframes studentSlideIn { + from { opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: none; } +} +@keyframes studentCardIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: none; } +} +@keyframes studentFadeUp { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} +@keyframes studentPopIn { + from { opacity: 0; transform: translateY(4px) scale(0.98); } + to { opacity: 1; transform: none; } +} + +/* ============================================================ + Mobile shell + ============================================================ */ +@media (max-width: 880px) { + body.student-mode .student-shell { grid-template-columns: 1fr; } + body.student-mode .student-sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 100; + width: 270px; + transform: translateX(-100%); + transition: transform 240ms var(--s-ease); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + } + body.student-mode .student-sidebar.open { transform: translateX(0); } + body.student-mode .student-topbar { padding: 12px 18px; } + body.student-mode .student-content { padding: 20px 18px; } +} + +/* ============================================================ + Conversation surface inside student shell + (preserves JS hooks: .conversation-container, .message-container, + #message-form, #content, .r-run-button, [data-message-id], + .r-execution-output) + ============================================================ */ +body.student-mode .conversation-container { color: var(--s-text); } +body.student-mode .message-container { + background: var(--s-surface-2); + border: 1px solid var(--s-border); + border-radius: 12px; + padding: 14px 16px; + margin-bottom: 14px; +} +body.student-mode .message-container .message-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + font-size: 12px; + color: var(--s-text-mute); +} +body.student-mode .message-container .message-header .text-muted { color: var(--s-text-mute) !important; } +body.student-mode .message-container .badge { + border-radius: 999px; + font-size: 11px; + padding: 3px 9px; +} +body.student-mode .message-container .badge.bg-primary { + background: var(--s-blue-soft) !important; + color: var(--s-blue-2); +} +body.student-mode .message-container .badge.bg-success { + background: var(--s-purple-soft) !important; + color: var(--s-purple); +} +body.student-mode .message-container .badge.bg-secondary { + background: var(--s-surface-3) !important; + color: var(--s-text-dim); +} +body.student-mode .message-container .badge.bg-info { + background: var(--s-blue-soft) !important; + color: var(--s-blue-2); +} +body.student-mode .message-container .message-content { + color: var(--s-text); + line-height: 1.6; +} +body.student-mode .message-container .message-content pre, +body.student-mode .message-container .message-content code { + background: var(--s-bg); + color: var(--s-text); + border-radius: 8px; +} +body.student-mode .r-code-container { + background: var(--s-bg); + border: 1px solid var(--s-border); + border-radius: 10px; + overflow: hidden; +} +body.student-mode .r-code-header { + background: var(--s-surface-3); + color: var(--s-text-dim); + border-bottom: 1px solid var(--s-border); +} +body.student-mode .r-code-container pre { + background: var(--s-bg); + color: var(--s-text); + margin: 0; + padding: 12px 14px; +} +body.student-mode .r-execution-output { + background: var(--s-surface-2); + color: var(--s-text); + border-top: 1px solid var(--s-border); +} + +/* Message input form inside student shell */ +body.student-mode #message-form .form-control, +body.student-mode #message-form textarea.form-control { + background: var(--s-bg); + color: var(--s-text); + border: 1px solid var(--s-border); + border-radius: 10px; + padding: 12px 14px; +} +body.student-mode #message-form .form-control:focus, +body.student-mode #message-form textarea.form-control:focus { + background: var(--s-surface-3); + border-color: var(--s-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + outline: none; +} +body.student-mode #message-form .form-control::placeholder { color: var(--s-text-mute); } +body.student-mode #message-form .btn-outline-primary { + color: var(--s-blue-2); + border-color: var(--s-blue); + background: transparent; + border-radius: 8px; +} +body.student-mode #message-form .btn-check:checked + .btn-outline-primary { + background: var(--s-blue); + color: white; +} +body.student-mode #message-form .btn-outline-success { + color: var(--s-green); + border-color: rgba(34,197,94,0.5); + background: transparent; + border-radius: 8px; +} +body.student-mode #message-form .btn-check:checked + .btn-outline-success { + background: var(--s-green); + color: white; +} + +/* Profile form Django widgets — inherit dark surfaces */ +body.student-mode .student-content input[type="text"], +body.student-mode .student-content input[type="email"], +body.student-mode .student-content input[type="password"] { + width: 100%; + background: var(--s-surface-2); + color: var(--s-text); + border: 1px solid var(--s-border); + border-radius: 10px; + padding: 10px 12px; + font-size: 14px; +} +body.student-mode .student-content input[type="text"]:focus, +body.student-mode .student-content input[type="email"]:focus, +body.student-mode .student-content input[type="password"]:focus { + background: var(--s-surface-3); + border-color: var(--s-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + outline: none; +} + +/* Sidebar toggle (only visible on mobile) */ +body.student-mode .student-sidebar-toggle { + display: none; + width: 38px; + height: 38px; + background: var(--s-surface); + border: 1px solid var(--s-border); + border-radius: 8px; + color: var(--s-text); + align-items: center; + justify-content: center; + cursor: pointer; +} +@media (max-width: 880px) { + body.student-mode .student-sidebar-toggle { display: inline-flex; } +} +body.student-mode .student-sidebar-scrim { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.55); + backdrop-filter: blur(2px); + z-index: 90; + opacity: 0; + pointer-events: none; + transition: opacity 200ms var(--s-ease); +} +body.student-mode .student-sidebar-scrim.show { + opacity: 1; + pointer-events: auto; +} diff --git a/static/css/teacher.css b/static/css/teacher.css new file mode 100644 index 0000000..a10a7d9 --- /dev/null +++ b/static/css/teacher.css @@ -0,0 +1,1096 @@ +/* ============================================================ + LLTeacher — Teacher dark modern theme + All rules scoped under body.teacher-mode so they never leak + into student / unauthenticated pages. + ============================================================ */ + +body.teacher-mode { + --t-bg: #0C1019; + --t-sidebar: #10151F; + --t-surface: #161B27; + --t-surface-2: #1C2434; + --t-surface-3: #232C40; + --t-border: #252F44; + --t-border-2: #313D55; + + --t-text: #F5F7FB; + --t-text-dim: #B5BAC6; + --t-text-mute: #7C8493; + + --t-blue: #3B82F6; + --t-blue-2: #60A5FA; + --t-blue-soft: rgba(59, 130, 246, 0.14); + + --t-green: #22C55E; + --t-green-soft: rgba(34, 197, 94, 0.14); + + --t-amber: #F59E0B; + --t-amber-soft: rgba(245, 158, 11, 0.14); + + --t-red: #EF4444; + --t-red-soft: rgba(239, 68, 68, 0.14); + + --t-purple: #8B5CF6; + --t-purple-soft: rgba(139, 92, 246, 0.14); + + --t-ease: cubic-bezier(0.2, 0.7, 0.2, 1); + + background-color: var(--t-bg); + color: var(--t-text); + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* ============================================================ + Hide legacy Bootstrap navbar + footer on shell pages + ============================================================ */ +body.teacher-mode:has(.teacher-shell) > nav.navbar, +body.teacher-mode:has(.teacher-shell) > footer.bg-light { + display: none !important; +} +body.teacher-mode:has(.teacher-shell) > main.container, +body.teacher-mode:has(.teacher-shell) > .container.mt-3 { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; +} +/* Surface Django-messages alerts as a polished bar that sits inline above + the shell's topbar instead of looking like a detached Bootstrap container. */ +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert { + border: 0 !important; + border-bottom: 1px solid var(--t-border) !important; + border-radius: 0 !important; + margin: 0 !important; + padding: 12px 32px !important; + background: var(--t-surface-2); + color: var(--t-text); + font-size: 13.5px; +} +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert.alert-success { + background: var(--t-green-soft); + color: var(--t-green); +} +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert.alert-warning { + background: var(--t-amber-soft); + color: var(--t-amber); +} +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert.alert-danger, +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert.alert-error { + background: var(--t-red-soft); + color: var(--t-red); +} +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .alert.alert-info { + background: var(--t-blue-soft); + color: var(--t-blue-2); +} +body.teacher-mode:has(.teacher-shell) > .container.mt-3 .btn-close { + filter: invert(1) opacity(0.7); +} + +/* ============================================================ + Teacher app shell layout + ============================================================ */ +body.teacher-mode .teacher-shell { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + min-height: 100vh; + background: var(--t-bg); +} + +body.teacher-mode .teacher-sidebar { + background: var(--t-sidebar); + border-right: 1px solid var(--t-border); + padding: 22px 14px 18px; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; +} +body.teacher-mode .teacher-sidebar .brand { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 8px 22px; + text-decoration: none; + color: var(--t-text); +} +body.teacher-mode .teacher-sidebar .brand-mark { + width: 34px; + height: 34px; + border-radius: 9px; + background: linear-gradient(135deg, var(--t-blue), var(--t-purple)); + display: grid; + place-items: center; + color: white; + font-weight: 700; + font-size: 13px; +} +body.teacher-mode .teacher-sidebar .brand-name { + font-weight: 600; + font-size: 15px; + display: block; +} +body.teacher-mode .teacher-sidebar .brand-sub { + font-size: 11px; + color: var(--t-text-mute); + display: block; + margin-top: 2px; +} + +body.teacher-mode .teacher-sidebar .nav-section { + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--t-text-mute); + padding: 14px 10px 6px; +} +body.teacher-mode .teacher-nav { + display: flex; + flex-direction: column; + gap: 2px; +} +body.teacher-mode .teacher-nav .nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 9px 10px; + border-radius: 9px; + color: var(--t-text-dim); + font-size: 14px; + text-decoration: none; + transition: background 150ms var(--t-ease), color 150ms var(--t-ease); +} +body.teacher-mode .teacher-nav .nav-item:hover { + background: var(--t-surface-2); + color: var(--t-text); +} +body.teacher-mode .teacher-nav .nav-item.active { + background: var(--t-surface-3); + color: var(--t-text); + box-shadow: inset 0 0 0 1px var(--t-border); +} +body.teacher-mode .teacher-nav .nav-item svg { + width: 16px; + height: 16px; + flex-shrink: 0; + opacity: 0.9; +} + +body.teacher-mode .teacher-sidebar .sidebar-foot { + margin-top: auto; + padding: 12px 8px 4px; + border-top: 1px solid var(--t-border); + display: flex; + align-items: center; + gap: 10px; +} +body.teacher-mode .teacher-sidebar .avatar { + width: 32px; height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, #2a3142, #444c63); + display: grid; place-items: center; + font-size: 12px; + font-weight: 600; + color: var(--t-text); + flex-shrink: 0; +} +body.teacher-mode .teacher-sidebar .who { min-width: 0; flex: 1; } +body.teacher-mode .teacher-sidebar .who-name { + font-size: 13px; + font-weight: 500; + color: var(--t-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +body.teacher-mode .teacher-sidebar .who-role { + font-size: 11px; + color: var(--t-text-mute); +} +body.teacher-mode .teacher-sidebar .sidebar-logout { + margin-left: auto; + width: 30px; height: 30px; + border-radius: 8px; + color: var(--t-text-mute); + display: grid; place-items: center; + transition: background 150ms var(--t-ease), color 150ms var(--t-ease); +} +body.teacher-mode .teacher-sidebar .sidebar-logout:hover { + background: var(--t-surface-2); + color: var(--t-text); +} + +/* Main column */ +body.teacher-mode .teacher-main { + display: flex; + flex-direction: column; + min-width: 0; +} +body.teacher-mode .teacher-topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + gap: 16px; + padding: 16px 32px; + border-bottom: 1px solid var(--t-border); + background: rgba(12, 16, 25, 0.88); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + min-height: 64px; +} +body.teacher-mode .topbar-crumbs { + font-size: 13px; + color: var(--t-text-mute); + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} +body.teacher-mode .topbar-crumbs a { + color: var(--t-text-dim); + text-decoration: none; +} +body.teacher-mode .topbar-crumbs a:hover { color: var(--t-text); } +body.teacher-mode .topbar-crumbs .crumb-now { color: var(--t-text); } +body.teacher-mode .topbar-crumbs .sep { color: var(--t-text-mute); } +body.teacher-mode .topbar-actions { display: flex; align-items: center; gap: 10px; } +body.teacher-mode .topbar-avatar { + width: 30px; height: 30px; + border-radius: 8px; + background: linear-gradient(135deg, #2a3142, #444c63); + display: grid; place-items: center; + font-size: 11px; + font-weight: 600; + color: var(--t-text); +} + +body.teacher-mode .teacher-content { + padding: 32px; + max-width: 1400px; + width: 100%; + margin: 0 auto; +} +@media (max-width: 768px) { + body.teacher-mode .teacher-content { padding: 20px 18px; } +} + +/* ============================================================ + Teacher page header + ============================================================ */ +body.teacher-mode .page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 28px; + flex-wrap: wrap; + animation: teacherFadeUp 380ms var(--t-ease) both; +} +body.teacher-mode .page-eyebrow { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--t-text-mute); + margin-bottom: 8px; +} +body.teacher-mode .page-title { + font-size: 28px; + font-weight: 500; + letter-spacing: -0.02em; + margin: 0 0 4px; + line-height: 1.15; + color: var(--t-text); +} +body.teacher-mode .page-sub { + color: var(--t-text-dim); + font-size: 14.5px; + margin: 0; + max-width: 640px; +} + +/* ============================================================ + Summary stat cards + ============================================================ */ +body.teacher-mode .stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + margin-bottom: 28px; +} +body.teacher-mode .stat-card { + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 14px; + padding: 18px 20px; + color: var(--t-text); + transition: border-color 200ms var(--t-ease); + animation: teacherCardIn 380ms var(--t-ease) both; +} +body.teacher-mode .stat-card:hover { border-color: var(--t-border-2); } +body.teacher-mode .stat-card .stat-label { + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--t-text-mute); + margin-bottom: 8px; +} +body.teacher-mode .stat-card .stat-value { + font-size: 28px; + font-weight: 500; + font-family: ui-monospace, "SF Mono", Menlo, monospace; + color: var(--t-text); + letter-spacing: -0.02em; + line-height: 1.1; +} +body.teacher-mode .stat-card .stat-value.green { color: var(--t-green); } +body.teacher-mode .stat-card .stat-value.amber { color: var(--t-amber); } +body.teacher-mode .stat-card .stat-value.red { color: var(--t-red); } +body.teacher-mode .stat-card .stat-value.blue { color: var(--t-blue-2); } +body.teacher-mode .stat-card .stat-sub { + font-size: 12px; + color: var(--t-text-mute); + margin-top: 4px; +} +/* Navigation-style stat card (no numeric value, just an action label). */ +body.teacher-mode .stat-card.t-nav-card { + display: block; + text-decoration: none; + color: inherit; +} +body.teacher-mode .stat-card.t-nav-card:hover { + border-color: var(--t-blue); + transform: translateY(-1px); + transition: border-color 150ms var(--t-ease), transform 150ms var(--t-ease); +} +body.teacher-mode .stat-card .nav-action { + font-size: 16px; + font-weight: 500; + color: var(--t-text); + line-height: 1.25; + letter-spacing: -0.01em; +} + +/* ============================================================ + Teacher cards (generic content) + ============================================================ */ +body.teacher-mode .teacher-card { + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 14px; + padding: 22px 24px; + color: var(--t-text); +} +body.teacher-mode .teacher-card .card-eyebrow { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--t-text-mute); + margin-bottom: 8px; +} +body.teacher-mode .teacher-card .card-title { + font-size: 17px; + font-weight: 500; + margin: 0 0 6px; + color: var(--t-text); +} + +body.teacher-mode .stagger > * { + opacity: 0; + animation: teacherCardIn 420ms var(--t-ease) both; +} +body.teacher-mode .stagger > *:nth-child(1) { animation-delay: 40ms; } +body.teacher-mode .stagger > *:nth-child(2) { animation-delay: 90ms; } +body.teacher-mode .stagger > *:nth-child(3) { animation-delay: 140ms; } +body.teacher-mode .stagger > *:nth-child(4) { animation-delay: 190ms; } +body.teacher-mode .stagger > *:nth-child(5) { animation-delay: 240ms; } +body.teacher-mode .stagger > *:nth-child(6) { animation-delay: 290ms; } + +/* ============================================================ + Status pills inside teacher shell + ============================================================ */ +body.teacher-mode .teacher-status { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 500; + padding: 3px 9px; + border-radius: 999px; + border: 1px solid var(--t-border); + background: var(--t-surface-2); + color: var(--t-text-dim); + white-space: nowrap; +} +body.teacher-mode .teacher-status .dot { + width: 6px; height: 6px; border-radius: 50%; background: currentColor; +} +body.teacher-mode .teacher-status.green { color: var(--t-green); background: var(--t-green-soft); border-color: rgba(34,197,94,0.3); } +body.teacher-mode .teacher-status.amber { color: var(--t-amber); background: var(--t-amber-soft); border-color: rgba(245,158,11,0.3); } +body.teacher-mode .teacher-status.red { color: var(--t-red); background: var(--t-red-soft); border-color: rgba(239,68,68,0.3); } +body.teacher-mode .teacher-status.blue { color: var(--t-blue-2); background: var(--t-blue-soft); border-color: rgba(59,130,246,0.3); } +body.teacher-mode .teacher-status.purple { color: var(--t-purple); background: var(--t-purple-soft); border-color: rgba(139,92,246,0.3); } +body.teacher-mode .teacher-status.mute { color: var(--t-text-mute); background: var(--t-surface-2); } + +/* ============================================================ + Teacher button utility class for shell pages + ============================================================ */ +body.teacher-mode .btn-t { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 9px 16px; + border-radius: 9px; + background: var(--t-surface-2); + border: 1px solid var(--t-border-2); + color: var(--t-text); + font-size: 13.5px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: background 150ms var(--t-ease), border-color 150ms var(--t-ease), transform 100ms var(--t-ease); +} +body.teacher-mode .btn-t:hover { + background: var(--t-surface-3); + border-color: #3a445a; + transform: translateY(-1px); + color: var(--t-text); +} +body.teacher-mode .btn-t-primary { + background: var(--t-blue); + border-color: var(--t-blue); + color: white; +} +body.teacher-mode .btn-t-primary:hover { + background: var(--t-blue-2); + border-color: var(--t-blue-2); + color: white; + box-shadow: 0 4px 16px -6px rgba(59,130,246,0.6); +} +body.teacher-mode .btn-t-sm { padding: 6px 12px; font-size: 12.5px; } +body.teacher-mode .btn-t-info { + background: var(--t-blue-soft); + border-color: rgba(59,130,246,0.4); + color: var(--t-blue-2); +} +body.teacher-mode .btn-t-info:hover { + background: rgba(59,130,246,0.22); + color: var(--t-blue-2); +} +body.teacher-mode .btn-t-warning { + background: var(--t-amber-soft); + border-color: rgba(245,158,11,0.4); + color: var(--t-amber); +} + +/* ============================================================ + Homework row used in teacher list + ============================================================ */ +body.teacher-mode .hw-row { + display: grid; + grid-template-columns: 56px minmax(0, 1fr) auto auto auto; + gap: 16px; + align-items: center; + padding: 16px 20px; + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 14px; + margin-bottom: 12px; + color: var(--t-text); + text-decoration: none; + transition: border-color 180ms var(--t-ease), transform 150ms var(--t-ease); +} +body.teacher-mode .hw-row:hover { + border-color: var(--t-border-2); + transform: translateY(-1px); + color: var(--t-text); +} +body.teacher-mode .hw-row .hw-date { + text-align: center; + font-family: ui-monospace, "SF Mono", Menlo, monospace; +} +body.teacher-mode .hw-row .hw-month { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--t-text-mute); +} +body.teacher-mode .hw-row .hw-day { + font-size: 20px; + font-weight: 500; + line-height: 1.1; + color: var(--t-text); +} +body.teacher-mode .hw-row .hw-main { min-width: 0; } +body.teacher-mode .hw-row .hw-title { + font-size: 15.5px; + color: var(--t-text); + font-weight: 500; + margin: 0; +} +body.teacher-mode .hw-row .hw-meta { + font-size: 12.5px; + color: var(--t-text-mute); + margin-top: 4px; +} +body.teacher-mode .hw-row .hw-actions { + display: flex; + gap: 8px; + align-items: center; +} + +/* ============================================================ + Section row inside teacher pages + ============================================================ */ +body.teacher-mode .teacher-section-row { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 12px; + color: var(--t-text); + text-decoration: none; + margin-bottom: 10px; + transition: border-color 180ms var(--t-ease), transform 150ms var(--t-ease); +} +body.teacher-mode .teacher-section-row:hover { + border-color: var(--t-border-2); + transform: translateY(-1px); + color: var(--t-text); +} +body.teacher-mode .teacher-section-row .leading { + width: 32px; height: 32px; + border-radius: 8px; + background: var(--t-surface-2); + display: grid; place-items: center; + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 12px; + color: var(--t-text-dim); + flex-shrink: 0; +} +body.teacher-mode .teacher-section-row .name { flex: 1; min-width: 0; font-size: 14.5px; } +body.teacher-mode .teacher-section-row .name .small { font-size: 12px; color: var(--t-text-mute); margin-top: 2px; } + +/* ============================================================ + Tables / lists inside teacher shell — re-skin Bootstrap chrome + ============================================================ */ +body.teacher-mode .teacher-content .alert.alert-info { + background: var(--t-blue-soft); + border-color: rgba(59,130,246,0.3); + color: var(--t-blue-2); + border-radius: 12px; +} +body.teacher-mode .teacher-content .alert.alert-warning { + background: var(--t-amber-soft); + border-color: rgba(245,158,11,0.3); + color: var(--t-amber); + border-radius: 12px; +} +body.teacher-mode .teacher-content .alert.alert-danger { + background: var(--t-red-soft); + border-color: rgba(239,68,68,0.3); + color: var(--t-red); + border-radius: 12px; +} +body.teacher-mode .teacher-content .form-control, +body.teacher-mode .teacher-content .form-select { + background: var(--t-surface-2); + color: var(--t-text); + border: 1px solid var(--t-border); + border-radius: 10px; +} +body.teacher-mode .teacher-content .form-control:focus, +body.teacher-mode .teacher-content .form-select:focus { + background: var(--t-surface-3); + border-color: var(--t-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + color: var(--t-text); + outline: none; +} +body.teacher-mode .teacher-content .form-control::placeholder { color: var(--t-text-mute); } +body.teacher-mode .teacher-content .text-muted { color: var(--t-text-mute) !important; } +body.teacher-mode .teacher-content .breadcrumb { background: transparent; padding: 0; } +body.teacher-mode .teacher-content .breadcrumb-item a { color: var(--t-text-dim); } +body.teacher-mode .teacher-content .breadcrumb-item.active { color: var(--t-text-mute); } + +/* Bootstrap input-group on submissions/matrix filters */ +body.teacher-mode .teacher-content .input-group-text { + background: var(--t-surface-2); + color: var(--t-text-dim); + border: 1px solid var(--t-border); +} + +/* Animations */ +@keyframes teacherCardIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } +} +@keyframes teacherFadeUp { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} + +/* Mobile shell */ +@media (max-width: 880px) { + body.teacher-mode .teacher-shell { grid-template-columns: 1fr; } + body.teacher-mode .teacher-sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 100; + width: 270px; + transform: translateX(-100%); + transition: transform 240ms var(--t-ease); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + } + body.teacher-mode .teacher-sidebar.open { transform: translateX(0); } + body.teacher-mode .teacher-topbar { padding: 12px 18px; } + body.teacher-mode .teacher-content { padding: 20px 18px; } +} + +/* Navbar */ +body.teacher-mode .navbar.bg-primary { + background: var(--t-surface) !important; + border-bottom: 1px solid var(--t-border); +} +body.teacher-mode .navbar .navbar-brand, +body.teacher-mode .navbar .nav-link { + color: var(--t-text) !important; +} +body.teacher-mode .navbar .nav-link.active { + color: var(--t-blue-2) !important; +} +body.teacher-mode .dropdown-menu { + background: var(--t-surface-2); + border: 1px solid var(--t-border); +} +body.teacher-mode .dropdown-item { color: var(--t-text-dim); } +body.teacher-mode .dropdown-item:hover, +body.teacher-mode .dropdown-item:focus { + background: var(--t-surface-3); + color: var(--t-text); +} + +/* Typography */ +body.teacher-mode h1, +body.teacher-mode h2, +body.teacher-mode h3, +body.teacher-mode h4, +body.teacher-mode h5, +body.teacher-mode h6 { + color: var(--t-text); + letter-spacing: -0.01em; +} +body.teacher-mode .lead, +body.teacher-mode .text-muted, +body.teacher-mode .breadcrumb-item.active, +body.teacher-mode small.text-muted, +body.teacher-mode .small.text-muted { + color: var(--t-text-mute) !important; +} +body.teacher-mode code { + background: var(--t-surface-2); + color: var(--t-blue-2); + padding: 2px 6px; + border-radius: 6px; +} + +/* Cards */ +body.teacher-mode .card { + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 14px; + color: var(--t-text); + box-shadow: 0 1px 2px rgba(0,0,0,0.3); + transition: border-color 200ms var(--t-ease); +} +body.teacher-mode .card:hover { border-color: var(--t-border-2); } +body.teacher-mode .card-header, +body.teacher-mode .card-header.bg-light, +body.teacher-mode .card-footer.bg-light { + background: var(--t-surface-2) !important; + border-bottom: 1px solid var(--t-border); + color: var(--t-text); +} +body.teacher-mode .card-footer { + background: var(--t-surface-2); + border-top: 1px solid var(--t-border); +} +body.teacher-mode .card.border-success { border-color: rgba(34,197,94,0.5); } +body.teacher-mode .card.border-danger { border-color: rgba(239,68,68,0.5); } +body.teacher-mode .card.border-primary { border-color: rgba(59,130,246,0.5); } +body.teacher-mode .card.border-warning { border-color: rgba(245,158,11,0.5); } +body.teacher-mode .card.border-info { border-color: rgba(59,130,246,0.4); } + +/* Buttons */ +body.teacher-mode .btn { + border-radius: 9px; + transition: background 150ms var(--t-ease), border-color 150ms var(--t-ease), transform 100ms var(--t-ease); +} +body.teacher-mode .btn:hover { transform: translateY(-1px); } +body.teacher-mode .btn-primary { background: var(--t-blue); border-color: var(--t-blue); } +body.teacher-mode .btn-primary:hover { + background: var(--t-blue-2); + border-color: var(--t-blue-2); + box-shadow: 0 4px 16px -6px rgba(59,130,246,0.6); +} +body.teacher-mode .btn-success { background: var(--t-green); border-color: var(--t-green); } +body.teacher-mode .btn-warning { background: var(--t-amber); border-color: var(--t-amber); color: #1a1a1a; } +body.teacher-mode .btn-danger { background: var(--t-red); border-color: var(--t-red); } +body.teacher-mode .btn-outline-primary { + color: var(--t-blue-2); + border-color: var(--t-blue); + background: transparent; +} +body.teacher-mode .btn-outline-primary:hover { + background: var(--t-blue); + color: white; +} +body.teacher-mode .btn-outline-secondary { + color: var(--t-text-dim); + border-color: var(--t-border-2); + background: transparent; +} +body.teacher-mode .btn-outline-secondary:hover { + background: var(--t-surface-3); + color: var(--t-text); + border-color: var(--t-border-2); +} +body.teacher-mode .btn-outline-info { + color: var(--t-blue-2); + border-color: rgba(59,130,246,0.5); +} +body.teacher-mode .btn-outline-warning { + color: var(--t-amber); + border-color: rgba(245,158,11,0.5); +} +body.teacher-mode .btn-outline-success { + color: var(--t-green); + border-color: rgba(34,197,94,0.5); +} +body.teacher-mode .btn-check:checked + .btn-outline-primary { + background: var(--t-blue); + color: white; +} +body.teacher-mode .btn-check:checked + .btn-outline-warning { + background: var(--t-amber); + color: #1a1a1a; +} +body.teacher-mode .btn-check:checked + .btn-outline-success { + background: var(--t-green); + color: white; +} +/* Selected-state styling when a Bootstrap btn-check radio is paired with + our custom .btn-t label (used by submissions filters). */ +body.teacher-mode .btn-check:checked + .btn-t { + background: var(--t-blue); + border-color: var(--t-blue); + color: white; + box-shadow: 0 0 0 1px var(--t-blue) inset; +} +body.teacher-mode .btn-check:checked + .btn-t.btn-t-warning { + background: var(--t-amber); + border-color: var(--t-amber); + color: #1a1a1a; +} +body.teacher-mode .btn-check:checked + .btn-t.btn-t-info { + background: var(--t-blue-2); + border-color: var(--t-blue-2); + color: white; +} +body.teacher-mode .btn-check:focus-visible + .btn-t { + outline: 2px solid var(--t-blue); + outline-offset: 2px; +} + +/* Badges */ +body.teacher-mode .badge.bg-primary { background: var(--t-blue-soft) !important; color: var(--t-blue-2); } +body.teacher-mode .badge.bg-success { background: var(--t-green-soft) !important; color: var(--t-green); } +body.teacher-mode .badge.bg-warning { background: var(--t-amber-soft) !important; color: var(--t-amber); } +body.teacher-mode .badge.bg-info { background: var(--t-blue-soft) !important; color: var(--t-blue-2); } +body.teacher-mode .badge.bg-danger { background: var(--t-red-soft) !important; color: var(--t-red); } +body.teacher-mode .badge.bg-secondary { background: var(--t-surface-3) !important; color: var(--t-text-dim); } + +/* Progress bars */ +body.teacher-mode .progress { + background: var(--t-surface-3); + border-radius: 999px; + height: 8px; +} +body.teacher-mode .progress-bar.bg-success { background: linear-gradient(90deg, var(--t-green), #34d399); } +body.teacher-mode .progress-bar.bg-warning { background: linear-gradient(90deg, var(--t-amber), #FBBF24); } + +/* Alerts */ +body.teacher-mode .alert.alert-info { + background: var(--t-blue-soft); + border-color: rgba(59,130,246,0.3); + color: var(--t-blue-2); +} +body.teacher-mode .alert.alert-success { + background: var(--t-green-soft); + border-color: rgba(34,197,94,0.3); + color: var(--t-green); +} +body.teacher-mode .alert.alert-warning { + background: var(--t-amber-soft); + border-color: rgba(245,158,11,0.3); + color: var(--t-amber); +} +body.teacher-mode .alert.alert-danger { + background: var(--t-red-soft); + border-color: rgba(239,68,68,0.3); + color: var(--t-red); +} + +/* Forms */ +body.teacher-mode .form-control, +body.teacher-mode .form-select, +body.teacher-mode textarea.form-control, +body.teacher-mode .input-group-text { + background: var(--t-surface-2); + color: var(--t-text); + border: 1px solid var(--t-border); + border-radius: 10px; +} +body.teacher-mode .form-control:focus, +body.teacher-mode .form-select:focus, +body.teacher-mode textarea.form-control:focus { + background: var(--t-surface-3); + border-color: var(--t-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); + color: var(--t-text); +} +body.teacher-mode .form-control::placeholder { color: var(--t-text-mute); } + +/* Breadcrumb */ +body.teacher-mode .breadcrumb { background: transparent; padding: 0; } +body.teacher-mode .breadcrumb-item a { color: var(--t-text-dim); } +body.teacher-mode .breadcrumb-item a:hover { color: var(--t-text); } + +/* Generic Bootstrap surface helpers used in shared markup */ +body.teacher-mode .bg-light, +body.teacher-mode .bg-white { + background: var(--t-surface-2) !important; + color: var(--t-text) !important; +} + +/* Tables (used by matrix.html) */ +body.teacher-mode .table { + color: var(--t-text); + border-color: var(--t-border); + --bs-table-bg: transparent; + --bs-table-color: var(--t-text); + --bs-table-border-color: var(--t-border); + --bs-table-striped-bg: var(--t-surface-2); + --bs-table-striped-color: var(--t-text); + --bs-table-hover-bg: var(--t-surface-3); + --bs-table-hover-color: var(--t-text); +} +body.teacher-mode .table th, +body.teacher-mode .table td { + border-color: var(--t-border); + background: transparent; +} +body.teacher-mode .table-bordered { border-color: var(--t-border); } + +/* Footer */ +body.teacher-mode footer.bg-light { + background: var(--t-surface) !important; + border-top: 1px solid var(--t-border); +} +body.teacher-mode footer.bg-light .text-muted { color: var(--t-text-mute) !important; } +body.teacher-mode footer.bg-light a { color: var(--t-blue-2); } + +/* ============================================================ + Submissions page (homeworks/submissions.html) — keeps its + participation classes, just retones them for dark. + ============================================================ */ +body.teacher-mode .student-card.no-interaction { + border-left: 4px solid var(--t-amber) !important; +} +body.teacher-mode .student-card.active { + border-left: 4px solid var(--t-green) !important; +} +body.teacher-mode .student-card.partial { + border-left: 4px solid var(--t-blue-2) !important; +} +body.teacher-mode .section-block { + background: var(--t-surface-2) !important; + border-color: var(--t-border) !important; + color: var(--t-text); + transition: border-color 150ms var(--t-ease), transform 100ms var(--t-ease); +} +body.teacher-mode .section-block:hover { + border-color: var(--t-border-2) !important; + transform: translateY(-1px); +} +body.teacher-mode .section-block.border-success { border-color: rgba(34,197,94,0.4) !important; } +body.teacher-mode .section-block.border-danger { border-color: rgba(239,68,68,0.4) !important; } +body.teacher-mode .bg-light-danger { + background: var(--t-red-soft) !important; + color: var(--t-red) !important; +} +body.teacher-mode .widget-block.bg-light { + background: var(--t-surface-3) !important; +} +body.teacher-mode .conversations-in-section { + border-top-color: var(--t-border); +} +/* Inner answer/conversation rows on submissions page */ +body.teacher-mode .answers-in-section > div, +body.teacher-mode .conversations-in-section > div { + background: var(--t-surface) !important; + border-color: var(--t-border) !important; + color: var(--t-text); +} + +/* ============================================================ + Matrix page (homeworks/matrix.html) + ============================================================ */ +body.teacher-mode .matrix-container { + border-color: var(--t-border); + background: var(--t-surface); +} +body.teacher-mode .matrix-table thead, +body.teacher-mode .matrix-table thead th { + background: var(--t-surface-2) !important; + color: var(--t-text); + border-bottom-color: var(--t-border) !important; +} +body.teacher-mode .matrix-table .sticky-cell { + background: var(--t-surface) !important; +} +body.teacher-mode .matrix-table thead .sticky-cell { + background: var(--t-surface-2) !important; +} +body.teacher-mode .student-cell { border-right-color: var(--t-border) !important; } +body.teacher-mode .student-name { color: var(--t-text); } +body.teacher-mode .student-email { color: var(--t-text-mute); } +body.teacher-mode .homework-due-date { color: var(--t-text-mute); } +body.teacher-mode .summary-header, +body.teacher-mode .summary-cell { + background: var(--t-surface-2) !important; + border-left-color: var(--t-border) !important; +} +body.teacher-mode .matrix-cell { + background: var(--t-surface); + color: var(--t-text); + transition: outline 120ms var(--t-ease), transform 120ms var(--t-ease); +} +body.teacher-mode .matrix-cell.status-submitted { background: rgba(34,197,94,0.10); } +body.teacher-mode .matrix-cell.status-in-progress { background: rgba(245,158,11,0.10); } +body.teacher-mode .matrix-cell.status-overdue { background: rgba(239,68,68,0.10); } +body.teacher-mode .matrix-cell.status-not-started { background: var(--t-surface); } +body.teacher-mode .matrix-cell:hover { + outline: 2px solid var(--t-blue); + transform: scale(1.03); +} +body.teacher-mode .legend-box { + display: inline-block; + width: 14px; + height: 14px; + border-radius: 4px; + margin-right: 8px; + vertical-align: middle; +} +body.teacher-mode .legend-box.status-submitted { background: rgba(34,197,94,0.35); } +body.teacher-mode .legend-box.status-in-progress { background: rgba(245,158,11,0.35); } +body.teacher-mode .legend-box.status-not-started { background: var(--t-surface-3); } +body.teacher-mode .legend-box.status-overdue { background: rgba(239,68,68,0.35); } + +/* ============================================================ + Conversation surface inside teacher shell + ============================================================ */ +body.teacher-mode .conversation-container { + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 12px; + padding: 14px; + max-height: 620px; + overflow-y: auto; +} +body.teacher-mode .message-container { + margin-bottom: 14px; +} +body.teacher-mode .message-container .message-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + color: var(--t-text-mute); + font-size: 12px; +} +body.teacher-mode .message-container .message-header small { + color: var(--t-text-mute); +} +body.teacher-mode .message-container .message-content { + padding: 12px 14px !important; + border-radius: 10px !important; + background: var(--t-surface-2); + border: 1px solid var(--t-border); + color: var(--t-text); + line-height: 1.6; +} +body.teacher-mode .message-chat .message-content { + background: rgba(59,130,246,0.10); + border-left: 3px solid var(--t-blue-2); + border-color: rgba(59,130,246,0.30); +} +body.teacher-mode .message-ai .message-content { + background: rgba(34,197,94,0.08); + border-left: 3px solid var(--t-green); + border-color: rgba(34,197,94,0.30); +} +body.teacher-mode .message-system .message-content { + background: var(--t-surface-2); + border-left: 3px solid var(--t-text-mute); +} +body.teacher-mode .r-code-container { + background: var(--t-surface); + border: 1px solid var(--t-border); + border-radius: 8px; + overflow: hidden; +} +body.teacher-mode .r-code-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--t-surface-3); + border-bottom: 1px solid var(--t-border); + color: var(--t-text-dim); + font-size: 12px; +} +body.teacher-mode .r-code-container pre, +body.teacher-mode .r-code-container code, +body.teacher-mode pre code.output { + background: var(--t-surface) !important; + color: var(--t-text) !important; +} +body.teacher-mode .message-type-selector .btn-check + .btn { + background: var(--t-surface-2); + color: var(--t-text-dim); + border-color: var(--t-border-2); +} +body.teacher-mode .message-type-selector .btn-check:checked + .btn { + background: var(--t-blue); + color: white; + border-color: var(--t-blue); +} +body.teacher-mode textarea.form-control { + background: var(--t-surface); + color: var(--t-text); + border: 1px solid var(--t-border); +} +body.teacher-mode textarea.form-control:focus { + background: var(--t-surface); + color: var(--t-text); + border-color: var(--t-blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.18); +} diff --git a/static/js/student-workspace.js b/static/js/student-workspace.js new file mode 100644 index 0000000..8e010cf --- /dev/null +++ b/static/js/student-workspace.js @@ -0,0 +1,137 @@ +/* ============================================================ + LLTeacher — Student workspace (frontend-only) + Powers the reasoning checker and the AI tutor quick-action + panel on the section workspace. No backend calls. + ============================================================ */ +(function () { + "use strict"; + + /* ---------- Reasoning checker ---------- */ + const wsRoot = document.querySelector("[data-student-workspace]"); + if (wsRoot) initReasoningChecker(wsRoot); + + /* ---------- Tutor quick-action panel ---------- */ + const tutorPanel = document.querySelector("[data-student-tutor]"); + if (tutorPanel) initTutorPanel(tutorPanel, wsRoot); + + /* ============================================================ */ + + function initReasoningChecker(root) { + const textarea = root.querySelector(".reasoning-textarea"); + const meterFill = root.querySelector(".reasoning-meter .meter-fill"); + const keywordChips = root.querySelectorAll(".reasoning-keyword"); + const checkBtn = root.querySelector("[data-check-reasoning]"); + const feedback = root.querySelector(".reasoning-feedback"); + if (!textarea) return; + + const KEYWORDS = [ + { word: "calculation", re: /\b(calcul|computed|computation)/i }, + { word: "formula", re: /\b(formula|equation|expression)\b/i }, + { word: "step", re: /\b(step|steps|first|then|next|finally)\b/i }, + { word: "because", re: /\b(because|since|therefore|so that|so,|thus|hence)\b/i }, + { word: "conclusion", re: /\b(answer|result|conclude|conclusion)\b/i }, + ]; + + function updateMeter() { + const text = textarea.value || ""; + let hits = 0; + KEYWORDS.forEach((k, i) => { + const matched = k.re.test(text); + const chip = keywordChips[i]; + if (chip) chip.classList.toggle("hit", matched); + if (matched) hits++; + }); + const pct = (hits / KEYWORDS.length) * 100; + if (meterFill) { + meterFill.style.width = pct + "%"; + if (pct >= 80) meterFill.style.background = "var(--s-green)"; + else if (pct >= 40) meterFill.style.background = "var(--s-amber)"; + else meterFill.style.background = "var(--s-red)"; + } + return { hits, total: KEYWORDS.length, length: text.trim().length }; + } + + function showFeedback(kind, title, body) { + if (!feedback) return; + feedback.className = "reasoning-feedback " + kind; + feedback.innerHTML = + '
' + title + '
' + body + '
'; + feedback.hidden = false; + } + + textarea.addEventListener("input", updateMeter); + + if (checkBtn) { + checkBtn.addEventListener("click", function (e) { + e.preventDefault(); + const r = updateMeter(); + if (r.length < 20) { + showFeedback("incorrect", "Write a bit more", + "Add a few sentences explaining how you worked through the problem before checking."); + return; + } + if (r.hits >= 4) { + showFeedback("correct", "Strong explanation", + "Your reasoning covers the key elements — the steps you took, why, and how you reached your conclusion."); + } else if (r.hits >= 2) { + showFeedback("partial", "Good start", + "You're on the right track. Try to be more explicit about your steps, the formula or rule you used, and why your answer follows from it."); + } else { + showFeedback("incorrect", "Explanation needs more detail", + "Walk through your calculation step by step. Mention the formula or rule you used and why your conclusion follows."); + } + }); + } + + updateMeter(); + // Expose meter helper for tutor panel + root.__getReasoningStats = updateMeter; + } + + function initTutorPanel(panel, wsRoot) { + const response = panel.querySelector(".tp-response"); + const responseText = panel.querySelector(".tp-response .tp-text"); + + const RESPONSES = { + hint: "Start by writing down what you're given. Then identify what you need to find. The path between often points to the formula you should use.", + formula: "Write the formula first, then substitute your values one step at a time. Say why each step follows from the one before it.", + missing: "Compare your reasoning to your final answer. Does each step explain why your conclusion follows? If a step is implicit, make it explicit.", + check: function () { + if (!wsRoot || typeof wsRoot.__getReasoningStats !== "function") { + return "Open the reasoning textarea above and write a few sentences first. I'll check whether your explanation covers the key steps."; + } + const r = wsRoot.__getReasoningStats(); + if (r.length < 20) { + return "Write a few sentences in your reasoning textarea first, then I can check whether your explanation covers the steps, the rule you used, and the conclusion."; + } + if (r.hits >= 4) { + return "Your reasoning is solid — it covers steps, the rule behind them, and how you reached your answer. Ready to discuss anything specific?"; + } + if (r.hits >= 2) { + return "Good direction. To make it stronger, be more explicit about the formula or rule you applied, and why your conclusion follows from it."; + } + return "Your explanation could use more structure. Walk through your steps one by one, name the rule or formula you used, and connect that to your answer."; + }, + }; + + function showResponse(text) { + if (!response || !responseText) return; + responseText.textContent = text; + response.hidden = false; + // Re-trigger entrance animation + response.style.animation = "none"; + void response.offsetWidth; + response.style.animation = ""; + } + + panel.addEventListener("click", function (e) { + const btn = e.target.closest("[data-tutor-action]"); + if (!btn) return; + e.preventDefault(); + const action = btn.dataset.tutorAction; + const value = RESPONSES[action]; + const text = typeof value === "function" ? value() : value; + if (text) showResponse(text); + }); + } +})(); diff --git a/templates/base.html b/templates/base.html index 40e74f7..1789bbb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,17 @@ {% load static %} - + + {% if request.user.is_authenticated %} + {% if request.user.teacher_profile %} + + {% elif request.user.student_profile %} + + {% endif %} + {% else %} + + {% endif %} + {% block extra_css %}{% endblock %} @@ -28,7 +38,7 @@ {% endif %} - +