Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/src/templates/studentSnapshot/StudentSnapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const StudentSnapshot: React.FC = () => {
teacherHasEnabledAi={aiTaEnabled}
studentId={selectedStudentId}
unitId={selectedUnitId}
sectionId={sectionId}
/>
<StudentCFUWidget
gridWidth={2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import i18n from '@cdo/locale';
import ActionButtons from './ActionButtons';
import FeedbackTextbox from './FeedbackTextbox';
import RecommendedActions from './RecommendedActions';
import {useLessonFeedback} from './useLessonFeedback';

import styles from './lessonFeeedback.module.scss';

Expand All @@ -17,6 +16,7 @@ interface LessonFeedbackWidgetProps {
teacherHasEnabledAi: boolean;
studentId: number | null;
unitId: number | null;
sectionId: number | null;
}

interface LessonFeedbackData {
Expand All @@ -34,6 +34,7 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
teacherHasEnabledAi = false,
studentId,
unitId,
sectionId,
}) => {
const [feedbackText, setFeedbackText] = React.useState<string>('');
const [existingFeedbackData, setExistingFeedbackData] =
Expand All @@ -51,17 +52,19 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
resource_link: '',
},
]);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLoading is set to true when the effect runs, but there’s no corresponding setIsLoading(false) shown after fetchLessonFeedback completes (or when the guard prevents fetching). This can leave the widget in a perpetual loading state. Consider moving setIsLoading(true) inside the if (lessonId && studentId && unitId && sectionId) block and ensuring setIsLoading(false) is set in a finally block within fetchLessonFeedback (and also in any early-return paths).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: I took this feedback


// Fetch lesson feedback from backend, and if not found, try generating ai feedback
React.useEffect(() => {
async function getAiLessonFeedback(
lessonId: number,
unitId: number,
studentId: number
studentId: number,
sectionId: number
) {
try {
const response = await fetch(
`/student_snapshots/ai_generated_lesson_feedback?lesson_id=${lessonId}&unit_id=${unitId}&student_id=${studentId}`
`/student_snapshots/ai_generated_lesson_feedback?lesson_id=${lessonId}&unit_id=${unitId}&student_id=${studentId}&section_id=${sectionId}`
);
if (!response.ok) {
throw new Error(
Expand All @@ -77,7 +80,7 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
}

async function fetchLessonFeedback() {
if (!lessonId || !studentId || !unitId) {
if (!lessonId || !studentId || !unitId || !sectionId) {
setFeedbackText('');
setResourceData([
{recommended_action: '', resource_name: '', resource_link: ''},
Expand All @@ -95,7 +98,12 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({

if (!response.ok) {
// Try getting AI feedback from student work.
const aiData = await getAiLessonFeedback(lessonId, unitId, studentId);
const aiData = await getAiLessonFeedback(
lessonId,
unitId,
studentId,
sectionId
);
if (aiData && aiData.json) {
const aiGeneratedInitialFeedback = JSON.parse(aiData.json).feedback;
setFeedbackText(aiGeneratedInitialFeedback);
Expand All @@ -122,15 +130,15 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
setResourceData([
{recommended_action: '', resource_name: '', resource_link: ''},
]);
} finally {
setIsLoading(false);
}
}
fetchLessonFeedback();
}, [lessonId, studentId, unitId]);
// Existing hook usage
const {isLoading, scrollable, handleSendToStudent} = useLessonFeedback({
lessonId,
teacherHasEnabledAi,
});
if (lessonId && studentId && unitId && sectionId) {
setIsLoading(true);
fetchLessonFeedback();
}
}, [lessonId, sectionId, studentId, unitId]);
Comment on lines 137 to 141
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLoading is set to true when the effect runs, but there’s no corresponding setIsLoading(false) shown after fetchLessonFeedback completes (or when the guard prevents fetching). This can leave the widget in a perpetual loading state. Consider moving setIsLoading(true) inside the if (lessonId && studentId && unitId && sectionId) block and ensuring setIsLoading(false) is set in a finally block within fetchLessonFeedback (and also in any early-return paths).

Copilot uses AI. Check for mistakes.

// Save as draft: update local state and persist to backend
const handleSaveAsDraft = async () => {
Expand Down Expand Up @@ -207,7 +215,9 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
/>
<ActionButtons
onSaveAsDraft={handleSaveAsDraft}
onSendToStudent={handleSendToStudent}
onSendToStudent={() => {
console.log('Send to student:', feedbackText, resourceData);
}}
Comment on lines +218 to +220
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a console.log in the user action handler will likely ship debug output to production and doesn’t perform the intended action. If sending isn’t implemented yet, consider wiring this back to the prior handler (or a stub that shows a UI message), or gating logs behind a dev-only check.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional for now, the "send to student" functionality will come later. The old handler was also just a console log statement.

/>
</div>
);
Expand All @@ -218,7 +228,7 @@ const LessonFeedbackWidget: React.FC<LessonFeedbackWidgetProps> = ({
gridWidth={2}
gridHeight={2}
loading={isLoading}
scrollable={scrollable}
scrollable={true}
>
{widgetContent}
</WidgetTemplate>
Expand Down

This file was deleted.

21 changes: 10 additions & 11 deletions dashboard/app/controllers/student_snapshots_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,19 @@ def ai_generated_lesson_feedback
lesson_id = params[:lesson_id]
unit_id = params[:unit_id]
student_id = params[:student_id]

section_id = nil
teacher_id = nil
if student_id && unit_id
student = User.find_by(id: student_id)
if student
section = student.sections_as_student.joins(:script).where(scripts: {id: unit_id}).first
section_id = section&.id
teacher_id = section&.teacher&.id
end
end
teacher_id = current_user.id
section_id = params[:section_id]

return render json: {error: "Missing required parameters"}, status: :bad_request unless lesson_id && unit_id && student_id && section_id
Comment on lines 31 to 37
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint now trusts section_id coming from the client. Because this value is user-controlled, it should be authorized server-side (e.g., ensure the section_id belongs to current_user, and the student_id is actually enrolled in that section) before using it to generate or fetch feedback. A concrete approach is to look up the section through current_user.sections (or equivalent) by section_id, and verify the student is in it; otherwise return a 403.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to do some research on this but I actually think this might be a good suggestion... I took it.


# Validate that the section belongs to the current teacher
section = Section.find_by(id: section_id)
return render json: {error: "Section not found"}, status: :not_found unless section

unless section.user_id == teacher_id || section.instructors.exists?(id: teacher_id)
return render json: {error: "Unauthorized access to section"}, status: :forbidden
end

response = AiStudentSnapshotHelper.generate_lesson_feedback(unit_id, lesson_id, teacher_id, student_id, section_id)

render json: response
Expand Down