diff --git a/apps/db/supabase/migrations/20250221165359_add_nova_tables.sql b/apps/db/supabase/migrations/20250221165359_add_nova_tables.sql new file mode 100644 index 0000000000..5bea0cbc2b --- /dev/null +++ b/apps/db/supabase/migrations/20250221165359_add_nova_tables.sql @@ -0,0 +1,267 @@ +create table "public"."nova_leaderboard" ( + "id" uuid not null default gen_random_uuid(), + "score" real, + "user_id" uuid, + "problem_id" text, + "created_at" timestamp with time zone not null default now() +); + +alter table + "public"."nova_leaderboard" enable row level security; + +create table "public"."nova_test_timer_record" ( + "id" uuid not null default gen_random_uuid(), + "duration" integer, + "problem_id" text, + "user_id" uuid, + "test_status" text, + "created_at" timestamp with time zone not null default now() +); + +alter table + "public"."nova_test_timer_record" enable row level security; + +create table "public"."nova_users_problem_history" ( + "id" bigint generated by default as identity not null, + "problem_id" text, + "user_id" uuid, + "score" real, + "feedback" text, + "user_prompt" text, + "problem_set_id" text, + "created_at" timestamp with time zone not null default now() +); + +alter table + "public"."nova_users_problem_history" enable row level security; + +CREATE UNIQUE INDEX nova_leaderboard_pkey ON public.nova_leaderboard USING btree (id); + +CREATE UNIQUE INDEX nova_test_timer_record_pkey ON public.nova_test_timer_record USING btree (id); + +CREATE UNIQUE INDEX nova_users_problem_history_pkey ON public.nova_users_problem_history USING btree (id); + +alter table + "public"."nova_leaderboard" +add + constraint "nova_leaderboard_pkey" PRIMARY KEY using index "nova_leaderboard_pkey"; + +alter table + "public"."nova_test_timer_record" +add + constraint "nova_test_timer_record_pkey" PRIMARY KEY using index "nova_test_timer_record_pkey"; + +alter table + "public"."nova_users_problem_history" +add + constraint "nova_users_problem_history_pkey" PRIMARY KEY using index "nova_users_problem_history_pkey"; + +alter table + "public"."nova_leaderboard" +add + constraint "nova_leaderboard_userId_fkey" FOREIGN KEY (user_id) REFERENCES workspace_users(id) not valid; + +alter table + "public"."nova_leaderboard" validate constraint "nova_leaderboard_userId_fkey"; + +alter table + "public"."nova_test_timer_record" +add + constraint "nova_test_timer_record_userId_fkey" FOREIGN KEY (user_id) REFERENCES users(id) not valid; + +alter table + "public"."nova_test_timer_record" validate constraint "nova_test_timer_record_userId_fkey"; + +alter table + "public"."nova_users_problem_history" +add + constraint "nova_users_problem_history_userId_fkey" FOREIGN KEY (user_id) REFERENCES users(id) not valid; + +alter table + "public"."nova_users_problem_history" validate constraint "nova_users_problem_history_userId_fkey"; + +grant delete on table "public"."nova_leaderboard" to "anon"; + +grant +insert + on table "public"."nova_leaderboard" to "anon"; + +grant references on table "public"."nova_leaderboard" to "anon"; + +grant +select + on table "public"."nova_leaderboard" to "anon"; + +grant trigger on table "public"."nova_leaderboard" to "anon"; + +grant truncate on table "public"."nova_leaderboard" to "anon"; + +grant +update + on table "public"."nova_leaderboard" to "anon"; + +grant delete on table "public"."nova_leaderboard" to "authenticated"; + +grant +insert + on table "public"."nova_leaderboard" to "authenticated"; + +grant references on table "public"."nova_leaderboard" to "authenticated"; + +grant +select + on table "public"."nova_leaderboard" to "authenticated"; + +grant trigger on table "public"."nova_leaderboard" to "authenticated"; + +grant truncate on table "public"."nova_leaderboard" to "authenticated"; + +grant +update + on table "public"."nova_leaderboard" to "authenticated"; + +grant delete on table "public"."nova_leaderboard" to "service_role"; + +grant +insert + on table "public"."nova_leaderboard" to "service_role"; + +grant references on table "public"."nova_leaderboard" to "service_role"; + +grant +select + on table "public"."nova_leaderboard" to "service_role"; + +grant trigger on table "public"."nova_leaderboard" to "service_role"; + +grant truncate on table "public"."nova_leaderboard" to "service_role"; + +grant +update + on table "public"."nova_leaderboard" to "service_role"; + +grant delete on table "public"."nova_test_timer_record" to "anon"; + +grant +insert + on table "public"."nova_test_timer_record" to "anon"; + +grant references on table "public"."nova_test_timer_record" to "anon"; + +grant +select + on table "public"."nova_test_timer_record" to "anon"; + +grant trigger on table "public"."nova_test_timer_record" to "anon"; + +grant truncate on table "public"."nova_test_timer_record" to "anon"; + +grant +update + on table "public"."nova_test_timer_record" to "anon"; + +grant delete on table "public"."nova_test_timer_record" to "authenticated"; + +grant +insert + on table "public"."nova_test_timer_record" to "authenticated"; + +grant references on table "public"."nova_test_timer_record" to "authenticated"; + +grant +select + on table "public"."nova_test_timer_record" to "authenticated"; + +grant trigger on table "public"."nova_test_timer_record" to "authenticated"; + +grant truncate on table "public"."nova_test_timer_record" to "authenticated"; + +grant +update + on table "public"."nova_test_timer_record" to "authenticated"; + +grant delete on table "public"."nova_test_timer_record" to "service_role"; + +grant +insert + on table "public"."nova_test_timer_record" to "service_role"; + +grant references on table "public"."nova_test_timer_record" to "service_role"; + +grant +select + on table "public"."nova_test_timer_record" to "service_role"; + +grant trigger on table "public"."nova_test_timer_record" to "service_role"; + +grant truncate on table "public"."nova_test_timer_record" to "service_role"; + +grant +update + on table "public"."nova_test_timer_record" to "service_role"; + +grant delete on table "public"."nova_users_problem_history" to "anon"; + +grant +insert + on table "public"."nova_users_problem_history" to "anon"; + +grant references on table "public"."nova_users_problem_history" to "anon"; + +grant +select + on table "public"."nova_users_problem_history" to "anon"; + +grant trigger on table "public"."nova_users_problem_history" to "anon"; + +grant truncate on table "public"."nova_users_problem_history" to "anon"; + +grant +update + on table "public"."nova_users_problem_history" to "anon"; + +grant delete on table "public"."nova_users_problem_history" to "authenticated"; + +grant +insert + on table "public"."nova_users_problem_history" to "authenticated"; + +grant references on table "public"."nova_users_problem_history" to "authenticated"; + +grant +select + on table "public"."nova_users_problem_history" to "authenticated"; + +grant trigger on table "public"."nova_users_problem_history" to "authenticated"; + +grant truncate on table "public"."nova_users_problem_history" to "authenticated"; + +grant +update + on table "public"."nova_users_problem_history" to "authenticated"; + +grant delete on table "public"."nova_users_problem_history" to "service_role"; + +grant +insert + on table "public"."nova_users_problem_history" to "service_role"; + +grant references on table "public"."nova_users_problem_history" to "service_role"; + +grant +select + on table "public"."nova_users_problem_history" to "service_role"; + +grant trigger on table "public"."nova_users_problem_history" to "service_role"; + +grant truncate on table "public"."nova_users_problem_history" to "service_role"; + +grant +update + on table "public"."nova_users_problem_history" to "service_role"; + +create policy "Enable all access for current user" on "public"."nova_leaderboard" as permissive for all to authenticated using ((user_id = auth.uid())) with check ((user_id = auth.uid())); + +create policy "Enable all access for current user" on "public"."nova_test_timer_record" as permissive for all to public using ((user_id = auth.uid())) with check ((user_id = auth.uid())); + +create policy "Enable all access for current user" on "public"."nova_users_problem_history" as permissive for all to public using ((user_id = auth.uid())) with check ((user_id = auth.uid())); \ No newline at end of file diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/challengeButton.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/challengeButton.tsx new file mode 100644 index 0000000000..d060126c06 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/challengeButton.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +interface CountdownTimerProps { + createdAt: string; + duration: number; + problemId: number; + wsId: string; + // onUpdateDuration: (remainingTime: number) => void; +} + +export default function CountdownTimer({ + createdAt, + duration, + problemId, + wsId, + // onUpdateDuration, +}: CountdownTimerProps) { + const router = useRouter(); + const [timeLeft, setTimeLeft] = useState(duration * 60); + console.log(duration); + useEffect(() => { + if (!createdAt || !duration) return; + + const startTime = new Date(createdAt).getTime(); + const endTime = startTime + duration * 60000; + + const updateTimer = () => { + const now = new Date().getTime(); + const remaining = Math.max(0, Math.floor((endTime - now) / 1000)); // Get remaining seconds + setTimeLeft(remaining); + + if (remaining === 0) { + // onUpdateDuration(0); + router.push(`/${wsId}/challenges/${problemId}/test-ended`); + } + }; + + const interval = setInterval(updateTimer, 1000); + updateTimer(); + + return () => clearInterval(interval); + }, [createdAt, duration, router]); + + const minutes = Math.floor(timeLeft / 60); + const seconds = timeLeft % 60; + + return ( +
+ Time Left: {minutes}:{seconds < 10 ? `0${seconds}` : seconds} +
+ ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/prompt-form.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/prompt-form.tsx new file mode 100644 index 0000000000..75193d2853 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/components/prompt-form.tsx @@ -0,0 +1,265 @@ +'use client'; + +import { Problems } from '../../challenges'; +import Mosaic from '@/components/common/LoadingIndicator'; +import { Button } from '@tutur3u/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@tutur3u/ui/dialog'; +import { Input } from '@tutur3u/ui/input'; +import { Separator } from '@tutur3u/ui/separator'; +import { History } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +type HistoryEntry = { + score: number; + feedback: string; + user_prompt: string; +}; +export default function ChatBox({ + problem, + challengeId, +}: { + problem: Problems; + challengeId: string; +}) { + const [_messages, setMessages] = useState< + { text: string; sender: 'user' | 'ai' }[] + >([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [_history, setHistory] = useState([]); + const [attempts, setAttempts] = useState(0); + + const router = useRouter(); + useEffect(() => { + const fetchHistory = async () => { + if (problem?.id) { + const fetchedHistory = await fetchProblemHistoryFromAPI(problem.id); + if (fetchedHistory) { + setHistory(fetchedHistory); + setAttempts(fetchedHistory.length); + } + } + }; + + fetchHistory(); + }, [problem?.id]); + + const updateProblemHistory = async ( + problemId: string, + feedback: string, + score: number, + user_prompt: string + ) => { + try { + const response = await fetch( + `/api/auth/workspace/${problemId}/nova/prompt`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ feedback, score, user_prompt, challengeId }), + } + ); + + if (!response.ok) { + throw new Error('Failed to update problem history'); + } + + const data = await response.json(); + console.log('Update Response:', data); + } catch (error) { + console.error('Error:', error); + } + }; + + const handleSend = async () => { + if (!input.trim() || loading || attempts >= 5) return; + + const newUserMessage = { text: input, sender: 'user' as const }; + setMessages((prev) => [...prev, newUserMessage]); + setInput(''); + setLoading(true); + setAttempts((prev) => prev + 1); + + try { + const response = await fetch('/api/ai/chat/google', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + problemDescription: problem.description, + testCase: problem.testcase, + answer: input, + exampleOutput: problem.exampleOutput, + exampleInput: problem.exampleInput, + }), + }); + + const data = await response.json(); + console.log(data); + + const aiResponseText = + data.response?.score !== undefined && data.response?.feedback + ? `Score: ${data.response.score}/10 - ${data.response.feedback}` + : "I couldn't process that. Try again!"; + + const newAIMessage = { text: aiResponseText, sender: 'ai' as const }; + setMessages((prev) => [...prev, newAIMessage]); + + if (data.response?.score !== undefined && data.response?.feedback) { + await updateProblemHistory( + problem.id, + data.response.feedback, + data.response.score, + input + ); + } + const fetchedHistory = await fetchProblemHistoryFromAPI(problem.id); + setHistory(fetchedHistory); + router.refresh(); + } catch (error) { + const errorMsg = { + text: 'An error occurred. Please try again.', + sender: 'ai' as const, + }; + setMessages((prev) => [...prev, errorMsg]); + } finally { + setLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSend(); + } + }; + + return ( +
+

Chat Box

+
+

+ You only have 5 tries for each question. [{attempts}/5] +

+ + + + + + + + + Problem History + + Below is the history of your attempts for this problem. + Feedbacks will be provided after you finish the test. + + + +
+ {_history && _history.length > 0 ? ( +
    + {_history.map((entry, index) => ( +
  • +
    + Attempt {index + 1}: + + Score: {entry.score}/10 + +
    +

    {entry.user_prompt}

    + +
  • + ))} +
+ ) : ( +

Nothing to show yet!

+ )} +
+ + + + +
+
+
+ +
+ {loading && ( +
+ +

We are processing your prompt please wait...

+
+ )} + + {!loading && _history.length > 0 && ( +
+

+ Your Last Attempt +

+
+
+
+

+ + Prompt:{' '} + + {_history[_history?.length - 1]?.user_prompt} +

+
+
+

+ + Score:{' '} + + {_history[_history?.length - 1]?.score}/10 +

+
+
+
+
+ )} + +
+
+ + {/* Chat Input */} +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading || attempts >= 5} + /> + +
+
+ ); +} + +async function fetchProblemHistoryFromAPI(problemId: string) { + const response = await fetch(`/api/auth/workspace/${problemId}/nova/prompt`); + const data = await response.json(); + + if (response.ok) { + return data; + } else { + console.log('Error fetching data'); + return null; + } +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/customizedHeader.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/customizedHeader.tsx new file mode 100644 index 0000000000..e8f9830335 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/customizedHeader.tsx @@ -0,0 +1,94 @@ +import CountdownTimer from './components/challengeButton'; +import ProblemChanger from './problem-changer'; +import LogoTitle from '@/app/[locale]/(marketing)/logo-title'; +import NavbarSeparator from '@/app/[locale]/(marketing)/navbar-separator'; +import { Button } from '@tutur3u/ui/button'; +import { cn } from '@tutur3u/utils/format'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { Suspense } from 'react'; + +interface Props { + proNum: number; + currentProblem: number; + createdAt: string; + duraion: number; + wsId: string; + onNext: () => void; + onPrev: () => void; +} + +export default function CustomizedHeader({ + proNum, + currentProblem, + onNext, + onPrev, + wsId, + createdAt, + duraion, +}: Props) { + const router = useRouter(); + + const handleEndTest = () => { + const confirmEnd = window.confirm('Are you sure you want to end the test?'); + if (confirmEnd) { + router.push('/'); + } + }; + + return ( + + ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/page.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/page.tsx new file mode 100644 index 0000000000..7a5b23f395 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/page.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { getChallenge } from '../challenges'; +import CustomizedHeader from './customizedHeader'; +import ProblemComponent from './problem-component'; +import PromptComponent from './prompt-component'; +import TestCaseComponent from './test-case-component'; +import { createClient } from '@tutur3u/supabase/next/client'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +interface Props { + params: Promise<{ + wsId: string; + challengeId: string; + }>; +} + +interface Timer { + duration: number; + created_at: string; +} + +export default function Page({ params }: Props) { + const [challenge, setChallenge] = useState(null); + const [fetchedTimer, setFetchedTimer] = useState( + null + ); + const [currentProblemIndex, setCurrentProblemIndex] = useState(0); + const [wsId, setWsId] = useState(''); + const [challengeId, setChallengeId] = useState(''); + const database = createClient(); + const router = useRouter(); + + useEffect(() => { + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + const confirmationMessage = + 'You have unsaved changes, are you sure you want to leave?'; + event.returnValue = confirmationMessage; + return confirmationMessage; + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); + + useEffect(() => { + const authCheck = async () => { + const { + data: { user }, + } = await database.auth.getUser(); + + if (!user?.id) { + router.push('/login'); + } + }; + + authCheck(); + }, []); + + useEffect(() => { + const fetchData = async () => { + const { wsId, challengeId } = await params; + const challengeData = getChallenge(parseInt(challengeId)); + setChallenge(challengeData); + setChallengeId(challengeId); + setWsId(wsId); + const timerData = await fetchTimer(String(challengeData?.id)); + setFetchedTimer(timerData); + }; + + fetchData(); + }, [params]); + + const problems = challenge?.problems || []; + + // Navigate to next problem + const nextProblem = () => { + setCurrentProblemIndex((prev) => + prev < problems.length - 1 ? prev + 1 : prev + ); + }; + + // Navigate to previous problem + const prevProblem = () => { + setCurrentProblemIndex((prev) => (prev > 0 ? prev - 1 : prev)); + }; + + return ( + <> + + +
+
+ {problems.length > 0 ? ( + + ) : ( +

No problems available.

+ )} + +
+ + +
+ + ); +} + +async function fetchTimer(problemId: string): Promise { + try { + console.log(problemId, 'id '); + const response = await fetch( + `/api/auth/workspace/${problemId}/nova/start-test` + ); + + if (!response.ok) { + throw new Error('Failed to fetch timer'); + } + const data = await response.json(); + return data; + } catch (error) { + console.log('Timer record error: ', error); + return null; + } +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-changer.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-changer.tsx new file mode 100644 index 0000000000..c7808c3870 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-changer.tsx @@ -0,0 +1,29 @@ +import { Button } from '@tutur3u/ui/button'; + +interface Props { + proNum: number; + currentProblem: number; + onNext: () => void; + onPrev: () => void; +} + +export default function ProblemChanger({ + proNum, + onNext, + onPrev, + currentProblem, +}: Props) { + return ( +
+ +
+ {currentProblem} of {proNum} +
+ +
+ ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-component.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-component.tsx new file mode 100644 index 0000000000..e544b76f97 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/problem-component.tsx @@ -0,0 +1,37 @@ +import { Card } from '@tutur3u/ui/card'; + +interface Problem { + id: string; + title: string; + description: string; + exampleInput: string; + exampleOutput: string; + constraints?: string[]; // Optional constraints field +} + +export default function ProblemComponent({ problem }: { problem: Problem }) { + return ( +
+ +

{problem.title}

+

{problem.description}

+

Example:

+
+          {`Input: s = "${problem.exampleInput}"\n\nOutput: "${problem.exampleOutput}"`}
+        
+ + {/* Render constraints if available */} + {problem.constraints && problem.constraints.length > 0 && ( + <> +

Constraints:

+
    + {problem.constraints.map((constraint, index) => ( +
  • {constraint}
  • + ))} +
+ + )} +
+
+ ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/prompt-component.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/prompt-component.tsx new file mode 100644 index 0000000000..d13028a8b9 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/prompt-component.tsx @@ -0,0 +1,17 @@ +import { Problems } from '../challenges'; +import ChatBox from './components/prompt-form'; +import { Card } from '@tutur3u/ui/card'; + +export default function PromptComponent({ + problem, + challengeId, +}: { + problem: Problems; + challengeId: string; +}) { + return ( + + + + ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-case-component.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-case-component.tsx new file mode 100644 index 0000000000..da15c84618 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-case-component.tsx @@ -0,0 +1,14 @@ +import { Card } from '@tutur3u/ui/card'; + +export default function TestCaseComponent({ testcase }: { testcase?: string }) { + return ( +
+ +

Test Case

+
+          {testcase}
+        
+
+
+ ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-ended/page.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-ended/page.tsx new file mode 100644 index 0000000000..2e3f885f30 --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/[challengeId]/test-ended/page.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { createClient } from '@tutur3u/supabase/next/client'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +interface Props { + params: Promise<{ challengeId: string }>; +} + +interface ReportData { + user_prompt: string; + score: number; + feedback: string; +} + +export default function Page({ params }: Props) { + const supabase = createClient(); + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); // Fix: Set type as an array of objects + const [problemId, setProblemId] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + const { challengeId } = await params; + setProblemId(challengeId); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user?.id) { + console.log('Unauthorized'); + router.push('/login'); + return; + } + + const response = await fetch( + `/api/auth/workspace/${challengeId}/nova/report` + ); + + if (!response.ok) { + throw new Error('Failed to fetch record data in page'); + } + + const result = await response.json(); + + // Ensure result is an array + const realData = Array.isArray(result) ? result : [result]; + + setData(realData); + } catch (err) { + console.error('Error fetching data:', err); + setError('There was an error fetching the data.'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [params, supabase.auth, router]); + + if (loading) { + return ( +
+

Loading...

+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + + return ( +
+

Problem Details

+

+ Problem ID: {problemId} +

+ +
+ + + + + + + + + + + {data.map((item, index) => ( + + + + + + + ))} + +
User StatementUser PromptScoreFeedback
{item.user_prompt}{item.user_prompt}{item.score}{item.feedback}
+
+
+ ); +} diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challengeCard.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challengeCard.tsx new file mode 100644 index 0000000000..2ccff5f95d --- /dev/null +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challengeCard.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { Challenge } from './challenges'; +import { Badge } from '@tutur3u/ui/badge'; +import { Button } from '@tutur3u/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@tutur3u/ui/card'; +import { ArrowRight, Star } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +interface ChallengeCardProps { + challenge: Challenge; + wsId: string; +} + +const ChallengeCard: React.FC = ({ challenge, wsId }) => { + const [isTestStarted, setIsTestStarted] = useState(false); + + useEffect(() => { + const checkTestStarted = async () => { + const response = await fetch( + `/api/auth/workspace/${challenge.id}/nova/start-test` + ); + const data = await response.json(); + + if (data?.test_status === 'START') { + setIsTestStarted(true); + } else { + setIsTestStarted(false); + } + }; + + checkTestStarted(); + }, [challenge.id, wsId]); + + const handleButton = async (event: React.MouseEvent) => { + event.preventDefault(); + + const confirmStart = window.confirm( + 'Are you sure you want to start this challenge?' + ); + + if (confirmStart) { + try { + const response = await fetch( + `/api/auth/workspace/${challenge.id}/nova/start-test`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + duration: challenge.duration, + test_status: 'START', + }), + } + ); + + if (!response.ok) { + throw new Error('Failed to update problem history'); + } + + window.location.href = `/${wsId}/challenges/${challenge.id}`; + } catch (error) { + console.error('Error: ', error); + } + } + }; + + const handleResumeTest = () => { + window.location.href = `/${wsId}/challenges/${challenge.id}`; + }; + + return ( + + + + {challenge.title} + {challenge.topic} + + + +

{challenge.description}

+
+ + + + + + Difficulty +
+
+ + + +
+ ); +}; + +export default ChallengeCard; diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challenges.ts b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challenges.ts index 6008b3fbd1..64d87911f7 100644 --- a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challenges.ts +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/challenges.ts @@ -1,50 +1,152 @@ export interface Challenge { - id: number; + id?: number; title: string; topic: string; description: string; + problems: Problems[]; + duration: number; +} + +export interface Problems { + id: string; + title: string; + description: string; exampleInput: string; exampleOutput: string; + constraints?: string[]; + testcase?: string[]; } const challenges: Challenge[] = [ { id: 1, - title: 'Text Summarization', + title: 'Text Suma', topic: 'Summarization', description: - 'Create a prompt that summarizes a given text while maintaining key information and context.', - exampleInput: - 'The Industrial Revolution was a period of major industrialization and innovation during the late 18th and early 19th century. The Industrial Revolution began in Great Britain and quickly spread throughout the world. This time period saw the mechanization of agriculture and textile manufacturing and a revolution in power, including steam ships and railroads, that affected social, cultural and economic conditions.', - exampleOutput: - 'The Industrial Revolution, occurring in the late 18th and early 19th centuries, was a time of significant technological and societal change. It started in Great Britain and spread globally, bringing mechanization to agriculture and textiles, and innovations in power like steam ships and railroads. These advancements had far-reaching impacts on society, culture, and the economy.', + 'Create prompts that summarize text while maintaining key details and context.', + problems: [ + { + id: '1a', + title: 'One-Sentence Summary', + description: 'Summarize a given text in just **one sentence**.', + exampleInput: + 'The Industrial Revolution was a period of major industrialization and innovation during the late 18th and early 19th century. It started in Great Britain and spread worldwide, introducing mechanized agriculture, textile manufacturing, and steam-powered transportation.', + exampleOutput: + 'The Industrial Revolution transformed industry with mechanization, starting in Britain and spreading globally.', + constraints: [ + '1 ≤ s.length ≤ 3 * 10⁵', + 's consists of printable ASCII characters.', + ], + testcase: [ + 'The Eiffel Tower is one of the most recognizable landmarks in the world, located in Paris, France, and completed in 1889.', + 'The Eiffel Tower is a famous Paris landmark, completed in 1889.', + ], + }, + { + id: '1b', + title: 'Summarization While Keeping Dates', + description: + 'Summarize a text without **removing key numbers or dates**.', + exampleInput: + 'In 2023, the global AI industry was valued at $150 billion, with an expected growth rate of 40% annually until 2030.', + exampleOutput: + 'In 2023, AI was a $150B industry, projected to grow 40% yearly until 2030.', + testcase: [ + 'In 1990, the world population was 5.32 billion, and it reached 7.9 billion by 2020.', + 'In 1990, the world population was 5.32B, growing to 7.9B by 2020.', + ], + }, + ], + duration: 60, }, { id: 2, title: 'Sentiment Analysis', topic: 'Classification', description: - 'Design a prompt that analyzes the sentiment of a given text, classifying it as positive, negative, or neutral.', - exampleInput: - "I absolutely loved the new restaurant downtown. The food was delicious and the service was impeccable. I can't wait to go back!", - exampleOutput: - 'This text expresses a very positive sentiment. The author uses enthusiastic language such as "absolutely loved" and "can\'t wait to go back". They praise both the food ("delicious") and the service ("impeccable"), indicating a highly satisfactory experience. Overall, this review conveys strong approval and excitement about the restaurant.', + 'Analyze the sentiment of text and classify it as positive, negative, or neutral.', + problems: [ + { + id: '2a', + title: 'Positive vs. Negative Sentiment', + description: 'Classify a given text as **positive or negative**.', + exampleInput: + 'I love this phone! The battery lasts forever, and the camera is amazing.', + exampleOutput: 'Positive', + testcase: [ + 'I absolutely hate the new software update; it’s so frustrating!', + 'Negative', + 'This is the best concert I’ve ever attended!', + 'Positive', + ], + }, + { + id: '2b', + title: 'Sarcasm Detection', + description: 'Identify whether a sentence is **sarcastic or not**.', + exampleInput: '"Oh great, another Monday! Just what I needed..."', + exampleOutput: 'Sarcastic', + testcase: [ + 'I love getting stuck in traffic for hours. What a fun way to spend my day!', + 'Sarcastic', + 'I’m thrilled that my package arrived on time.', + 'Not Sarcastic', + ], + }, + ], + duration: 45, // Added duration (in minutes) }, { id: 3, title: 'Code Explanation', topic: 'Programming', - description: - 'Create a prompt that explains a given piece of code in simple terms, suitable for beginners.', - exampleInput: ` -def fibonacci(n): - if n <= 1: - return n - else: - return fibonacci(n-1) + fibonacci(n-2) - `, - exampleOutput: - 'This code defines a function called "fibonacci" that calculates the nth number in the Fibonacci sequence. The Fibonacci sequence is a series of numbers where each number is the sum of the two preceding ones. The function uses recursion, which means it calls itself with smaller values until it reaches the base case (n <= 1). For larger values of n, it calculates the Fibonacci number by adding the two previous numbers in the sequence. This recursive approach, while simple to understand, can be inefficient for large values of n due to repeated calculations.', + description: 'Explain programming concepts in simple terms.', + problems: [ + { + id: '3a', + title: 'Explain a For Loop', + description: + 'Explain the function of a **for loop** in a beginner-friendly way.', + exampleInput: ` +for (let i = 0; i < 5; i++) { + console.log(i); +} + `, + exampleOutput: + 'This loop runs five times, printing numbers from 0 to 4. It starts at `i = 0`, increases `i` by 1 each time, and stops at `i = 5`.', + testcase: [ + ` +for (let i = 1; i <= 3; i++) { + console.log(i); +} + `, + 'This loop runs three times, printing numbers from 1 to 3. It starts at `i = 1` and stops when `i` exceeds 3.', + ], + }, + { + id: '3b', + title: 'Explain Recursion', + description: 'Explain **recursion** using a simple example.', + exampleInput: ` +function factorial(n) { + if (n === 1) return 1; + return n * factorial(n - 1); +} + `, + exampleOutput: + 'This function calculates the factorial of a number by calling itself repeatedly until it reaches the base case (`n === 1`).', + testcase: [ + ` +function fibonacci(n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + `, + 'This function calculates the nth Fibonacci number by calling itself twice for each value of n until it reaches the base case where n <= 1.', + ], + }, + ], + duration: 75, // Added duration (in minutes) }, ]; diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/page.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/page.tsx index a2dfac2633..09a996051f 100644 --- a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/page.tsx +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/challenges/page.tsx @@ -1,27 +1,24 @@ +import ChallengeCard from './challengeCard'; import { getChallenges } from './challenges'; import { createClient } from '@tutur3u/supabase/next/server'; -import { Badge } from '@tutur3u/ui/badge'; -import { Button } from '@tutur3u/ui/button'; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from '@tutur3u/ui/card'; -import { ArrowRight, Star } from 'lucide-react'; -import Link from 'next/link'; import { redirect } from 'next/navigation'; -export default async function ChallengesPage() { +interface Props { + params: Promise<{ + wsId: string; + }>; +} + +export default async function ChallengesPage({ params }: Props) { const database = await createClient(); const { data: { user }, } = await database.auth.getUser(); - + const { wsId } = await params; if (!user?.id) { redirect('/login'); } + const challenges = getChallenges(); return ( @@ -29,39 +26,11 @@ export default async function ChallengesPage() {

Prompt Engineering Challenges

{challenges.map((challenge) => ( - - - - {challenge.title} - {challenge.topic} - - - -

- {challenge.description} -

-
- - - - - - - Difficulty - -
-
- - - - - -
+ ))}
diff --git a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/layout.tsx b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/layout.tsx index 867214b7ae..9eefeef655 100644 --- a/apps/nova/src/app/[locale]/(dashboard)/[wsId]/layout.tsx +++ b/apps/nova/src/app/[locale]/(dashboard)/[wsId]/layout.tsx @@ -1,21 +1,30 @@ +'use client'; + import { Sidebar } from '@/components/layout/sidebar'; import { Toaster } from '@tutur3u/ui/toaster'; -import type { Metadata } from 'next'; import { ThemeProvider } from 'next-themes'; import { Inter } from 'next/font/google'; +import { usePathname } from 'next/navigation'; const inter = Inter({ subsets: ['latin'] }); -export const metadata: Metadata = { - title: 'Prompt Engineering Playground', - description: 'Experiment with AI prompts and challenges', -}; - export default function RootLayout({ children, }: { children: React.ReactNode; }) { + const pathName = usePathname(); + + const SIDEBAR_HIDDEN_ROUTES = ['/login', '/signup']; + const SIDEBAR_HIDDEN_ROUTE_PATTERNS = [ + /^\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\/challenges\/\d+$/, // Matches UUID/challenges/{id} + ]; + + const shouldShowSidebar = !( + SIDEBAR_HIDDEN_ROUTES.includes(pathName) || + SIDEBAR_HIDDEN_ROUTE_PATTERNS.some((pattern) => pattern.test(pathName)) + ); + return (
- + {shouldShowSidebar && }
{children}
diff --git a/apps/nova/src/app/[locale]/login/form.tsx b/apps/nova/src/app/[locale]/login/form.tsx index 51d8f4b4a0..875aba8f7c 100644 --- a/apps/nova/src/app/[locale]/login/form.tsx +++ b/apps/nova/src/app/[locale]/login/form.tsx @@ -64,7 +64,7 @@ export default function LoginForm() { // if on DEV_MODE, auto-open inbucket if (DEV_MODE) { window.open( - window.location.origin.replace('7803', '8004') + '/monitor', + window.location.origin.replace('7805', '8004') + '/monitor', '_blank' ); } @@ -125,8 +125,13 @@ export default function LoginForm() { }); if (res.ok) { - const nextUrl = searchParams.get('nextUrl'); - router.push(nextUrl ?? '/onboarding'); + const nextUrl = searchParams.get('nextUrl') ?? '/onboarding'; + console.log('Before redirect:', nextUrl); + + // Save to localStorage to persist after refresh + localStorage.setItem('nextUrl', nextUrl); + + router.push(nextUrl); router.refresh(); } else { setLoading(false); @@ -141,6 +146,14 @@ export default function LoginForm() { } }; + useEffect(() => { + const storedNextUrl = localStorage.getItem('nextUrl'); + if (storedNextUrl) { + console.log('After refresh:', storedNextUrl); + localStorage.removeItem('nextUrl'); // Clear after logging + } + }, []); + async function onSubmit(data: z.infer) { const { email, otp } = data; diff --git a/apps/nova/src/app/api/ai/chat/google/route.ts b/apps/nova/src/app/api/ai/chat/google/route.ts new file mode 100644 index 0000000000..2d8eac36d0 --- /dev/null +++ b/apps/nova/src/app/api/ai/chat/google/route.ts @@ -0,0 +1,99 @@ +import { GoogleGenerativeAI } from '@google/generative-ai'; +import { NextResponse } from 'next/server'; + +// Replace this with your actual API key +const API_KEY = process.env.GOOGLE_GENERATIVE_AI_API_KEY || ''; + +// Initialize Google Generative AI Client +const genAI = new GoogleGenerativeAI(API_KEY); + +export const runtime = 'edge'; +export const maxDuration = 60; +export const preferredRegion = 'sin1'; + +export async function POST(req: Request) { + const { + answer, + problemDescription, + testCase, + model = 'gemini-pro', + exampleInput, + exampleOutput, + } = (await req.json()) as { + answer?: string; + problemDescription?: string; + testCase?: string; + model?: string; + exampleInput?: string; + exampleOutput?: string; + }; + + try { + if (!answer || !problemDescription || !testCase) { + return NextResponse.json( + { message: 'Incomplete data provided.' }, + { status: 400 } + ); + } + + // System Instruction for Evaluation with strict JSON output + const systemInstruction = ` +You are an examiner in a prompt engineering competition. +You will be provided with a problem description and a test case. The user will input a prompt or an answer designed to solve the problem. +Your role is to evaluate the user's response based on how effectively it guides or solves the problem. + +Problem: ${problemDescription} +Test Case: ${testCase} +Example Input: ${exampleInput} +Example Output: ${exampleOutput} +User's Answer: ${answer} + +Scoring Criteria: +- **10**: The user's response perfectly solves the problem or provides a clear and effective prompt that would solve the problem. +- **7-9**: The response mostly solves the problem or gives a good prompt but with minor inefficiencies or missing details. +- **4-6**: The response shows some understanding but has notable errors, incomplete results, or inefficient approaches. +- **1-3**: The response attempts to address the problem but is mostly incorrect, irrelevant, or incomplete. +- **0**: The response is entirely irrelevant or simply repeats the problem description without guiding towards a solution. + +Important Notes: +1. **If the user's response is an effective prompt** that guides solving the problem (e.g., "Summarize the paragraph in just one sentence"), it should be **scored based on how well it would solve the task**. +2. Only assign **0** if the response does not attempt to solve the problem or is irrelevant. +3. Ensure the feedback clearly explains why the score was assigned, focusing on how well the response addresses the problem. +4. Only respond with the following JSON format: +{ + "score": [number from 0 to 10], + "feedback": "[brief explanation of the score]" +} +`; + + // Get the model + const aiModel = genAI.getGenerativeModel({ model }); + + // Send the instruction to Google API + const result = await aiModel.generateContent(systemInstruction); + const response = result.response.text(); + + let parsedResponse; + try { + parsedResponse = JSON.parse(response); + // eslint-disable-next-line no-unused-vars + } catch (parseError) { + return NextResponse.json( + { + message: 'AI response was not in valid JSON format.', + rawResponse: response, + }, + { status: 200 } + ); + } + + return NextResponse.json({ response: parsedResponse }, { status: 200 }); + } catch (error: any) { + return NextResponse.json( + { + message: `API Failure\nCould not complete the request.\n\n${error?.stack}`, + }, + { status: 500 } + ); + } +} diff --git a/apps/nova/src/app/api/auth/workspace/[problemId]/nova/prompt/route.ts b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/prompt/route.ts new file mode 100644 index 0000000000..049615308c --- /dev/null +++ b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/prompt/route.ts @@ -0,0 +1,80 @@ +import { createClient } from '@tutur3u/supabase/next/server'; +import { NextResponse } from 'next/server'; + +interface Params { + params: Promise<{ + problemId: string; + }>; +} + +export async function GET(_: Request, { params }: Params) { + const supabase = await createClient(); + const { problemId: id } = await params; + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user?.id) { + console.log('Unauthorized'); + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { data, error } = await supabase + .from('nova_users_problem_history') + .select('score, feedback, user_prompt') + .eq('problem_id', id) + .eq('user_id', user.id); + + if (error) { + return NextResponse.json( + { message: 'Error fetching problem history' }, + { status: 500 } + ); + } + + return NextResponse.json(data); +} + +export async function POST(req: Request, { params }: Params) { + const supabase = await createClient(); + + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser(); + if (!user) { + console.log('Unauthorized'); + return; + } + if (authError) { + console.log(authError); + } + + const { problemId: id } = await params; + + const { feedback, score, user_prompt, challengeId } = await req.json(); + + const upsertData = { + user_id: user?.id, + problem_id: id, + feedback: feedback || '', + score: score || 0, + user_prompt: user_prompt, + problem_set_id: challengeId || '', + }; + + const { error } = await supabase + .from('nova_users_problem_history') + .upsert(upsertData); + + if (error) { + console.log(error); + return NextResponse.json( + { message: 'Error updating problem history' }, + { status: 500 } + ); + } + + return NextResponse.json({ message: 'Problem history updated successfully' }); +} diff --git a/apps/nova/src/app/api/auth/workspace/[problemId]/nova/report/route.ts b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/report/route.ts new file mode 100644 index 0000000000..cd905bca7d --- /dev/null +++ b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/report/route.ts @@ -0,0 +1,44 @@ +import { createClient } from '@tutur3u/supabase/next/server'; +import { NextResponse } from 'next/server'; + +interface Params { + params: Promise<{ + problemId: string; + }>; +} + +export async function GET(_: Request, { params }: Params) { + const supabase = await createClient(); + const { problemId } = await params; + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + console.log('Unauthorized'); + } + + if (!user?.id) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { data, error } = await supabase + .from('nova_users_problem_history') + .select('user_prompt, score, problem_id, feedback') + .eq('problem_set_id', problemId); + + if (error) { + console.log(error); + return NextResponse.json( + { + message: 'Error fetching record in route', + }, + { + status: 500, + } + ); + } + + return NextResponse.json(data); +} diff --git a/apps/nova/src/app/api/auth/workspace/[problemId]/nova/start-test/route.ts b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/start-test/route.ts new file mode 100644 index 0000000000..451970b04b --- /dev/null +++ b/apps/nova/src/app/api/auth/workspace/[problemId]/nova/start-test/route.ts @@ -0,0 +1,114 @@ +import { createClient } from '@tutur3u/supabase/next/server'; +import { NextResponse } from 'next/server'; + +interface Params { + params: Promise<{ + problemId: string; + }>; +} + +export async function GET(_: Request, { params }: Params) { + const supabase = await createClient(); + const { problemId } = await params; + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user?.id) { + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const { data, error } = await supabase + .from('nova_test_timer_record') + .select('duration, created_at, test_status') + .eq('problem_id', problemId) + .eq('user_id', user.id); + + if (error) { + console.log(error); + return NextResponse.json( + { message: 'Error fetching timer in route' }, + { status: 500 } + ); + } + + return NextResponse.json(data); +} + +export async function POST(req: Request, { params }: Params) { + const supabase = await createClient(); + const { problemId } = await params; + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + console.log('Unauthorized'); + return; + } + + const { duration, test_status } = await req.json(); + + const upsertData = { + user_id: user?.id, + problem_id: problemId, + duration: duration, + test_status: test_status, + }; + + const { error } = await supabase + .from('nova_test_timer_record') + .upsert(upsertData); + + if (error) { + console.log(error); + return NextResponse.json( + { message: 'Error updating problem history' }, + { status: 500 } + ); + } + return NextResponse.json({ message: 'Problem history updated successfully' }); +} + +export async function PUT(req: Request, { params }: Params) { + const supabase = await createClient(); + const { problemId } = await params; + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + console.log('Unauthorized'); + return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + } + + const userId = user?.id; + const { test_status } = await req.json(); + + if (!test_status) { + return NextResponse.json( + { message: 'Missing test_status' }, + { status: 400 } + ); + } + + // Update the test status + const { error } = await supabase + .from('nova_test_timer_record') + .update({ test_status }) + .eq('problem_id', problemId) + .eq('user_id', userId); + + if (error) { + console.log(error); + return NextResponse.json( + { message: 'Error updating test status' }, + { status: 500 } + ); + } + + return NextResponse.json({ message: 'Test status updated successfully' }); +} diff --git a/apps/nova/src/components/common/LoadingIndicator.tsx b/apps/nova/src/components/common/LoadingIndicator.tsx index 5631f6ff74..15fe9fe799 100644 --- a/apps/nova/src/components/common/LoadingIndicator.tsx +++ b/apps/nova/src/components/common/LoadingIndicator.tsx @@ -17,7 +17,7 @@ export default function LoadingIndicator({ className="opacity-25" cx="12" cy="12" - r="10" + r="20" stroke="currentColor" strokeWidth="4" > diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index 01b9c4ada2..ab71ac05d4 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -2023,6 +2023,128 @@ export type Database = { }, ]; }; + nova_leaderboard: { + Row: { + created_at: string; + id: string; + problem_id: string | null; + score: number | null; + user_id: string | null; + }; + Insert: { + created_at?: string; + id?: string; + problem_id?: string | null; + score?: number | null; + user_id?: string | null; + }; + Update: { + created_at?: string; + id?: string; + problem_id?: string | null; + score?: number | null; + user_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'nova_leaderboard_userId_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'distinct_invoice_creators'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'nova_leaderboard_userId_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'workspace_users'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'nova_leaderboard_userId_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'workspace_users_with_groups'; + referencedColumns: ['id']; + }, + ]; + }; + nova_test_timer_record: { + Row: { + created_at: string; + duration: number | null; + id: string; + problem_id: string | null; + test_status: string | null; + user_id: string | null; + }; + Insert: { + created_at?: string; + duration?: number | null; + id?: string; + problem_id?: string | null; + test_status?: string | null; + user_id?: string | null; + }; + Update: { + created_at?: string; + duration?: number | null; + id?: string; + problem_id?: string | null; + test_status?: string | null; + user_id?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'nova_test_timer_record_userId_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'users'; + referencedColumns: ['id']; + }, + ]; + }; + nova_users_problem_history: { + Row: { + created_at: string; + feedback: string | null; + id: number; + problem_id: string | null; + problem_set_id: string | null; + score: number | null; + user_id: string | null; + user_prompt: string | null; + }; + Insert: { + created_at?: string; + feedback?: string | null; + id?: number; + problem_id?: string | null; + problem_set_id?: string | null; + score?: number | null; + user_id?: string | null; + user_prompt?: string | null; + }; + Update: { + created_at?: string; + feedback?: string | null; + id?: number; + problem_id?: string | null; + problem_set_id?: string | null; + score?: number | null; + user_id?: string | null; + user_prompt?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'nova_users_problem_history_userId_fkey'; + columns: ['user_id']; + isOneToOne: false; + referencedRelation: 'users'; + referencedColumns: ['id']; + }, + ]; + }; personal_notes: { Row: { content: string | null;