From 3f7a4d74e46531fa897a12377639924de495cebe Mon Sep 17 00:00:00 2001 From: jaimenguyen168 <77992599+jaimenguyen168@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:48:41 -0500 Subject: [PATCH 1/2] Download all users data by class --- src/api/classes.ts | 2 - src/components/DownloadFormattedFile.tsx | 16 +- src/components/TypingLogs.tsx | 2 +- .../ClassTypingLogsDownloadButton.tsx | 230 ++++++++++++++++++ .../ui/views/admin/ClassStatView.tsx | 23 +- 5 files changed, 247 insertions(+), 26 deletions(-) create mode 100644 src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx diff --git a/src/api/classes.ts b/src/api/classes.ts index c73a168b..ff5f5e79 100755 --- a/src/api/classes.ts +++ b/src/api/classes.ts @@ -279,8 +279,6 @@ export async function getClassActivityByInstructorId( return { data: [] }; } - console.log("Log data", JSON.stringify(logs, null, 2)); - return { data: logs }; } catch (err) { return { diff --git a/src/components/DownloadFormattedFile.tsx b/src/components/DownloadFormattedFile.tsx index 06048f90..e8aed46e 100644 --- a/src/components/DownloadFormattedFile.tsx +++ b/src/components/DownloadFormattedFile.tsx @@ -1,4 +1,4 @@ -import { Download } from "lucide-react"; +import { Download, Loader2 } from "lucide-react"; import { saveAs } from "file-saver"; import Papa from "papaparse"; import { Button } from "./ui/button"; @@ -9,6 +9,7 @@ interface DownloadFormattedFileProps { data: any[]; filename?: string; disabled?: boolean; + loading?: boolean; } /** @@ -20,6 +21,7 @@ const DownloadFormattedFile = ({ data, filename = "data", disabled = false, + loading = false, }: DownloadFormattedFileProps) => { const [format, setFormat] = useState<"csv" | "json">("csv"); @@ -62,10 +64,14 @@ const DownloadFormattedFile = ({ border-0" disabled={disabled} > -
- - Download {format.toUpperCase()} -
+ {loading ? ( + + ) : ( +
+ + Download {format.toUpperCase()} +
+ )} ); diff --git a/src/components/TypingLogs.tsx b/src/components/TypingLogs.tsx index 84150b80..ba956679 100644 --- a/src/components/TypingLogs.tsx +++ b/src/components/TypingLogs.tsx @@ -486,7 +486,7 @@ const TypingLogs = ({ userId }: TypingLogsProps) => { className="px-4 py-2 bg-primary text-primary-foreground rounded-lg disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed hover:bg-primary/90 transition-colors flex items-center gap-2" > - Reload All Records + Reload All Records diff --git a/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx b/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx new file mode 100644 index 00000000..60217bd5 --- /dev/null +++ b/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx @@ -0,0 +1,230 @@ +import DownloadFormattedFile from "@/components/DownloadFormattedFile"; +import { supabase } from "@/supabaseClient"; +import { User } from "@/types/user"; +import { useEffect, useState } from "react"; + +interface ClassTypingLogsDownloadButtonProps { + classId: string; + className?: string; +} + +type TypingLogData = { + id: string; + created_at: string; + raw_text: string; + line_suggestion_id: string | null; + user_id: string; + event: string; + line_suggestions?: { + id: string; + correct_line: string | null; + incorrect_line: string | null; + shown_bug: boolean | null; + bug_percentage: number | null; + line_suggestions_group?: { + filename: string | null; + language: string | null; + } | null; + } | null; +}; + +type ClassUser = { + student_id: string; + users: User; +}; + +const ClassTypingLogsDownloadButton = ({ + classId, + className, +}: ClassTypingLogsDownloadButtonProps) => { + const [typingData, setTypingData] = useState([]); + const [loading, setLoading] = useState(false); + const [userCount, setUserCount] = useState(0); + + const focusEvents = [ + "TYPING", + "SUGGESTION_SHOWN", + "SUGGESTION_TAB_ACCEPT", + "RUN", + "SUGGESTION_LINE_REJECT", + "SUGGESTION_GENERATE", + ]; + + useEffect(() => { + const fetchClassTypingData = async () => { + if (!classId) { + return; + } + + setLoading(true); + + try { + const { data: classUsers, error: classError } = await supabase + .from("user_class") + .select( + ` + student_id, + users:student_id ( + id, + first_name, + last_name, + pid, + email + ) + `, + ) + .eq("class_id", classId); + + if (classError) { + throw classError; + } + + const users = classUsers as unknown as ClassUser[]; + setUserCount(users.length); + + if (users.length === 0) { + setTypingData([]); + return; + } + + const allFormattedData: any[] = []; + + for (const classUser of users) { + const userId = classUser.student_id; + const user = classUser.users; + + const userLogs: TypingLogData[] = []; + let from = 0; + const batchSize = 1000; + let hasMoreData = true; + + while (hasMoreData) { + const { data, error } = await supabase + .from("typing_log") + .select( + ` + id, + created_at, + raw_text, + line_suggestion_id, + user_id, + event, + line_suggestions:line_suggestion_id ( + id, + correct_line, + incorrect_line, + shown_bug, + bug_percentage, + line_suggestions_group:group_id ( + filename, + language + ) + ) + `, + ) + .eq("user_id", userId) + .in("event", focusEvents) + .order("created_at", { ascending: true }) + .range(from, from + batchSize - 1); + + if (error) { + throw error; + } + + const logs = data as unknown as TypingLogData[]; + userLogs.push(...logs); + + hasMoreData = logs.length === batchSize; + from += batchSize; + + if (hasMoreData) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } + + userLogs.sort( + (a, b) => + new Date(a.created_at).getTime() - + new Date(b.created_at).getTime(), + ); + + const userFormattedData = + userLogs?.map((log, index) => { + let timeDifference = 0; + if (index > 0) { + const currentTime = new Date(log.created_at).getTime(); + const previousTime = new Date( + userLogs[index - 1].created_at, + ).getTime(); + timeDifference = currentTime - previousTime; + } + + return { + "No.": index + 1, + "User ID": userId, + PID: user?.pid || "N/A", + Username: user?.firstName || "N/A", + "Last Name": user?.lastName || "N/A", + Email: user?.email || "N/A", + Event: log.event, + Timestamp: new Date(log.created_at).getTime(), + "Time Difference (ms)": timeDifference, + "Raw Text": log.raw_text.replace(/\n/g, "\\n"), + "Correct Line": log.line_suggestions?.correct_line || "N/A", + "Incorrect Line": log.line_suggestions?.incorrect_line || "N/A", + "Bug Shown": log.line_suggestions?.shown_bug ?? "N/A", + "Bug Percentage": log.line_suggestions?.bug_percentage ?? "N/A", + Filename: + log.line_suggestions?.line_suggestions_group?.filename || + "N/A", + Language: + log.line_suggestions?.line_suggestions_group?.language || + "N/A", + }; + }) || []; + + allFormattedData.push(...userFormattedData); + } + + allFormattedData.sort((a, b) => + a["User ID"].localeCompare(b["User ID"]), + ); + + let currentUserId = ""; + let userRowNumber = 1; + allFormattedData.forEach((row) => { + if (row["User ID"] !== currentUserId) { + currentUserId = row["User ID"]; + userRowNumber = 1; + } + row["No."] = userRowNumber++; + }); + + setTypingData(allFormattedData); + } catch (err) { + console.error("Error fetching class typing data:", err); + } finally { + setLoading(false); + } + }; + + fetchClassTypingData(); + }, [classId]); + + const filename = className + ? `class-typing-logs-${className.replace(/\s+/g, "-")}` + : `class-typing-logs-${classId}`; + + return ( +
+ +
+ ); +}; + +export default ClassTypingLogsDownloadButton; diff --git a/src/pages/dashboard/ui/views/admin/ClassStatView.tsx b/src/pages/dashboard/ui/views/admin/ClassStatView.tsx index acdf5223..97ebc272 100644 --- a/src/pages/dashboard/ui/views/admin/ClassStatView.tsx +++ b/src/pages/dashboard/ui/views/admin/ClassStatView.tsx @@ -1,13 +1,13 @@ -import DownloadFormattedFile from "@/components/DownloadFormattedFile"; import ActivityStatsSection from "../../components/ActivityStatsSection"; import { useParams } from "react-router-dom"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useClassActivity } from "@/pages/dashboard/hooks/useClassActivity"; import { UserMode, UserRole } from "@/types/user"; import { ClassDetailsCard } from "../../components/ClassDetailsCard"; import { useClassData } from "@/pages/classes/hooks/useClassData"; import { Edit } from "lucide-react"; import { Button } from "@/components/ui/button"; +import ClassTypingLogsDownloadButton from "@/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx"; const ClassStatView = ({}) => { const { instructorId, classId } = useParams(); @@ -25,25 +25,12 @@ const ClassStatView = ({}) => { UserMode.LINE_BY_LINE, ); - const formatDataForDownload = useMemo(() => { - return classActivity.map((activity, index) => ({ - "No.": index + 1, - "User ID": activity.userId, - Event: activity.event, - "Class Title": activity.classTitle || "N/A", - "Class Code": activity.classCode || "N/A", - "Duration (seconds)": activity.duration, - "Has Bug": activity.hasBug ? "Yes" : "No", - Type: activity.type || "N/A", - "Created At": new Date(activity.createdAt).toLocaleString(), - })); - }, [classActivity, classId]); return (
-
From d6ff4cabc6d1b2ac33436d5a514852579ced9e46 Mon Sep 17 00:00:00 2001 From: jaimenguyen168 <77992599+jaimenguyen168@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:24:35 -0500 Subject: [PATCH 2/2] Embed run terminal log --- src/components/TypingLogsDownloadButton.tsx | 63 ++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/components/TypingLogsDownloadButton.tsx b/src/components/TypingLogsDownloadButton.tsx index 46f8cd8a..ad8be481 100644 --- a/src/components/TypingLogsDownloadButton.tsx +++ b/src/components/TypingLogsDownloadButton.tsx @@ -20,7 +20,7 @@ type TypingLogData = { correct_line: string | null; incorrect_line: string | null; shown_bug: boolean | null; - bug_percentage: number | null; // Add this field + bug_percentage: number | null; line_suggestions_group?: { filename: string | null; language: string | null; @@ -28,6 +28,14 @@ type TypingLogData = { } | null; }; +type TerminalLogData = { + id: string; + start_time: string; + end_time: string; + output: string; + user_id: string; +}; + const TypingLogsDownloadButton = ({ userId, user, @@ -44,6 +52,25 @@ const TypingLogsDownloadButton = ({ "SUGGESTION_GENERATE", ]; + // Function to find matching terminal output for a given timestamp + const findMatchingTerminalOutput = ( + eventTimestamp: string, + terminalLogs: TerminalLogData[], + ): string | null => { + const eventTime = new Date(eventTimestamp).getTime(); + + for (const terminalLog of terminalLogs) { + const startTime = new Date(terminalLog.start_time).getTime(); + const endTime = new Date(terminalLog.end_time).getTime(); + + if (eventTime >= startTime && eventTime <= endTime) { + return terminalLog.output; + } + } + + return null; + }; + useEffect(() => { const fetchTypingData = async () => { if (!userId) return; @@ -51,12 +78,26 @@ const TypingLogsDownloadButton = ({ setLoading(true); try { + // First, fetch terminal logs for the user + const { data: terminalLogsData, error: terminalLogsError } = + await supabase + .from("terminal_logs") + .select("id, start_time, end_time, output, user_id") + .eq("user_id", userId) + .order("start_time", { ascending: true }); + + if (terminalLogsError) { + throw terminalLogsError; + } + + const terminalLogs = terminalLogsData as TerminalLogData[]; + + // Then fetch typing logs const allLogs: TypingLogData[] = []; let from = 0; const batchSize = 1000; let hasMoreData = true; - // Fetch all records in batches while (hasMoreData) { const { data, error } = await supabase .from("typing_log") @@ -93,11 +134,9 @@ const TypingLogsDownloadButton = ({ const logs = data as unknown as TypingLogData[]; allLogs.push(...logs); - // Check if we got a full batch, if not, we're done hasMoreData = logs.length === batchSize; from += batchSize; - // Add a small delay to prevent overwhelming the server if (hasMoreData) { await new Promise((resolve) => setTimeout(resolve, 50)); } @@ -114,6 +153,12 @@ const TypingLogsDownloadButton = ({ timeDifference = currentTime - previousTime; } + // Find matching terminal output + const terminalOutput = findMatchingTerminalOutput( + log.created_at, + terminalLogs, + ); + return { "No.": index + 1, PID: user?.pid || "N/A", @@ -125,11 +170,14 @@ const TypingLogsDownloadButton = ({ "Correct Line": log.line_suggestions?.correct_line || "N/A", "Incorrect Line": log.line_suggestions?.incorrect_line || "N/A", "Bug Shown": log.line_suggestions?.shown_bug ?? "N/A", - "Bug Percentage": log.line_suggestions?.bug_percentage ?? "N/A", // Changed this line + "Bug Percentage": log.line_suggestions?.bug_percentage ?? "N/A", Filename: log.line_suggestions?.line_suggestions_group?.filename || "N/A", Language: log.line_suggestions?.line_suggestions_group?.language || "N/A", + "Terminal Output": terminalOutput + ? terminalOutput.replace(/\n/g, "\\n") + : "N/A", }; }) || []; @@ -142,7 +190,7 @@ const TypingLogsDownloadButton = ({ }; fetchTypingData(); - }, [userId, user]); // Removed userSettings dependency + }, [userId, user]); const filename = user?.pid ? `typing-logs-${user.firstName}-${user.pid}` @@ -152,7 +200,8 @@ const TypingLogsDownloadButton = ({ ); };