diff --git a/src/api/classes.ts b/src/api/classes.ts index b1c191d6..c73a168b 100755 --- a/src/api/classes.ts +++ b/src/api/classes.ts @@ -10,7 +10,7 @@ import { ClassPaginationParams } from "@/types/data"; * @returns {Promise<{ data?: { id: string }; error?: string }>} - The response from the server */ export const createClass = async ( - newClass: ClassInfo + newClass: ClassInfo, ): Promise<{ id?: string; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/`, { @@ -44,7 +44,7 @@ export const createClass = async ( export const updateClass = async ( classId: string, - updates: Partial + updates: Partial, ): Promise<{ id?: string; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/${classId}`, { @@ -77,7 +77,7 @@ export const updateClass = async ( }; export const deleteClass = async ( - classId: string + classId: string, ): Promise<{ success?: boolean; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/${classId}`, { @@ -116,7 +116,7 @@ export const deleteClass = async ( */ export const getClassesByInstructor = async ( instructorId: string, - includeStudents: boolean = false + includeStudents: boolean = false, ): Promise<{ data?: ClassData[]; error?: string }> => { try { const url = new URL(`${CLASS_ENDPOINT}/instructor/${instructorId}`); @@ -188,7 +188,7 @@ export async function getClassesbyStudent(studentId: string): Promise<{ */ export const registerUserClass = async ( userId: string, - classId: string + classId: string, ): Promise<{ success: boolean; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/register`, { @@ -219,7 +219,7 @@ export const registerUserClass = async ( export const unregisterUserClass = async ( userId: string, - classId: string + classId: string, ): Promise<{ success: boolean; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/unregister`, { @@ -252,7 +252,7 @@ export const unregisterUserClass = async ( * @returns {Promise<{ data?: UserActivityLogItem[]; error?: string }>} - The response from the server or an error message. */ export async function getClassActivityByInstructorId( - instructorId: string + instructorId: string, ): Promise<{ data: ActivityLogResponse; error?: string }> { console.log("Fetching class activity for instructor:", instructorId); @@ -279,6 +279,8 @@ export async function getClassActivityByInstructorId( return { data: [] }; } + console.log("Log data", JSON.stringify(logs, null, 2)); + return { data: logs }; } catch (err) { return { @@ -298,7 +300,7 @@ export async function getClassActivityByInstructorId( export const updateStudentEnrollmentStatus = async ( classId: string, studentId: string, - newStatus: EnrollmentStatus + newStatus: EnrollmentStatus, ): Promise<{ success: boolean; error?: string }> => { try { const response = await fetch(`${CLASS_ENDPOINT}/enrollment-status`, { @@ -342,7 +344,7 @@ interface PaginatedClassResponse { } export async function getAllClasses( - params: ClassPaginationParams = {} + params: ClassPaginationParams = {}, ): Promise<{ data?: PaginatedClassResponse; error?: string; @@ -392,7 +394,7 @@ export async function getAllClasses( export async function updateClassStudentsSettings( classId: string, studentIds: string[], - settings: UserSettings + settings: UserSettings, ): Promise<{ data?: boolean; error?: string }> { try { const response = await fetch( @@ -404,7 +406,7 @@ export async function updateClassStudentsSettings( studentIds, settings, }), - } + }, ); const data = await response.json(); @@ -429,7 +431,7 @@ export async function getClassById( includeStudents?: boolean; userId?: string; includeAllStatuses?: boolean; - } + }, ): Promise<{ data?: ClassData; error?: string; @@ -440,7 +442,7 @@ export async function getClassById( if (options?.includeStudents) { searchParams.append( "includeStudents", - options.includeStudents.toString() + options.includeStudents.toString(), ); } @@ -451,7 +453,7 @@ export async function getClassById( if (options?.includeAllStatuses) { searchParams.append( "includeAllStatuses", - options.includeAllStatuses.toString() + options.includeAllStatuses.toString(), ); } diff --git a/src/components/TypingLogs.tsx b/src/components/TypingLogs.tsx index 7989d298..84150b80 100644 --- a/src/components/TypingLogs.tsx +++ b/src/components/TypingLogs.tsx @@ -22,6 +22,8 @@ import { SkipForward, SkipBack, Bot, + RefreshCw, + Loader2, } from "lucide-react"; type TypingLogData = { @@ -56,7 +58,7 @@ interface TypingLogsProps { userId: string; } -const TypingLogs: React.FC = ({ userId }) => { +const TypingLogs = ({ userId }: TypingLogsProps) => { const [timeline, setTimeline] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -69,6 +71,12 @@ const TypingLogs: React.FC = ({ userId }) => { const [selectedEventType, setSelectedEventType] = useState("SUGGESTION_SHOWN"); + // Pagination state - removed maxRecords since we're fetching all + const [totalRecordsFound, setTotalRecordsFound] = useState(0); + const [fetchCancelled, setFetchCancelled] = useState(false); + const [batchesFetched, setBatchesFetched] = useState(0); + const [loadingProgress, setLoadingProgress] = useState(""); + // Focus on these 5 events const focusEvents = [ "TYPING", @@ -114,14 +122,30 @@ const TypingLogs: React.FC = ({ userId }) => { fetchUser(); }, [userId]); - useEffect(() => { - const fetchTypingLogs = async () => { - if (!userId) return; - - setLoading(true); - setError(null); + const fetchTypingLogs = async () => { + if (!userId) return; + + setLoading(true); + setError(null); + setFetchCancelled(false); + setTotalRecordsFound(0); + setBatchesFetched(0); + setLoadingProgress("Initializing..."); + + try { + const allLogs: TypingLogData[] = []; + let from = 0; + const batchSize = 1000; + let hasMoreData = true; + let totalFetched = 0; + + while (hasMoreData && !fetchCancelled) { + const currentBatch = Math.floor(from / batchSize) + 1; + setBatchesFetched(currentBatch); + setLoadingProgress( + `Fetching batch ${currentBatch}... (${totalFetched.toLocaleString()} records so far)`, + ); - try { const { data, error } = await supabase .from("typing_log") .select( @@ -142,41 +166,74 @@ const TypingLogs: React.FC = ({ userId }) => { ) .eq("user_id", userId) .in("event", focusEvents) - .order("created_at", { ascending: true }); + .order("created_at", { ascending: true }) + .range(from, from + batchSize - 1); if (error) { throw error; } const logs = data as unknown as TypingLogData[]; + allLogs.push(...logs); + totalFetched += logs.length; - // Process logs into timeline events - const timelineEvents: TimelineEvent[] = logs.map((log) => ({ - id: log.id, - timestamp: log.created_at, - type: log.event === "TYPING" ? "typing" : "suggestion_event", - content: log.raw_text, - event: log.event || "TYPING", // Default to TYPING - suggestionData: log.line_suggestions - ? { - correctLine: log.line_suggestions.correct_line || null, - incorrectLine: log.line_suggestions.incorrect_line || null, - shownBug: log.line_suggestions.shown_bug ?? null, - } - : undefined, - })); - - setTimeline(timelineEvents); - } catch (err) { - console.error("Error fetching typing logs:", err); - setError( - err instanceof Error ? err.message : "Failed to fetch typing logs", - ); - } finally { - setLoading(false); + setTotalRecordsFound(totalFetched); + + // 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)); + } + } + + if (fetchCancelled) { + return; } - }; + setLoadingProgress( + `Processing ${totalFetched.toLocaleString()} records...`, + ); + + // Process logs into timeline events + const timelineEvents: TimelineEvent[] = allLogs.map((log) => ({ + id: log.id, + timestamp: log.created_at, + type: log.event === "TYPING" ? "typing" : "suggestion_event", + content: log.raw_text, + event: log.event || "TYPING", // Default to TYPING + suggestionData: log.line_suggestions + ? { + correctLine: log.line_suggestions.correct_line || null, + incorrectLine: log.line_suggestions.incorrect_line || null, + shownBug: log.line_suggestions.shown_bug ?? null, + } + : undefined, + })); + + setTimeline(timelineEvents); + setCurrentLogIndex(0); // Reset to first record + } catch (err) { + console.error("Error fetching typing logs:", err); + setError( + err instanceof Error ? err.message : "Failed to fetch typing logs", + ); + } finally { + setLoading(false); + setLoadingProgress(""); + } + }; + + const cancelFetch = () => { + setFetchCancelled(true); + setLoading(false); + setLoadingProgress(""); + }; + + // Trigger fetch when userId changes + useEffect(() => { fetchTypingLogs(); }, [userId]); @@ -346,6 +403,8 @@ const TypingLogs: React.FC = ({ userId }) => { // Add keyboard navigation useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { + if (loading) return; // Don't navigate while loading + switch (event.key) { case "ArrowLeft": event.preventDefault(); @@ -360,20 +419,7 @@ const TypingLogs: React.FC = ({ userId }) => { window.addEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress); - }, [currentLogIndex, timeline.length]); - - if (loading) { - return ( - - -
- - Loading typing logs... - -
-
- ); - } + }, [currentLogIndex, timeline.length, loading]); if (!userId) { return ( @@ -398,6 +444,13 @@ const TypingLogs: React.FC = ({ userId }) => {

Error: {error}

+ ); @@ -422,21 +475,57 @@ const TypingLogs: React.FC = ({ userId }) => { - {timeline.length} Events + {timeline.length.toLocaleString()} Events + + - {timeline.length === 0 ? ( + {/* Loading State */} + {loading && ( + + +
+ +
+

+ Loading All Typing Logs +

+

+ {loadingProgress} +

+
+ Batches: {batchesFetched} + + Records: {totalRecordsFound.toLocaleString()} +
+
+
+
+
+ )} + + {timeline.length === 0 && !loading ? (

No typing logs found for this user.

+

+ Check if the user has any activity in the system. +

- ) : ( + ) : timeline.length > 0 && !loading ? ( <> {/* Navigation Controls Card */} @@ -463,7 +552,8 @@ const TypingLogs: React.FC = ({ userId }) => {
- {currentLogIndex + 1} / {timeline.length} + {(currentLogIndex + 1).toLocaleString()} /{" "} + {timeline.length.toLocaleString()}