diff --git a/app/[chapter]/[subchapter]/questions/[question]/page.tsx b/app/[chapter]/[subchapter]/questions/[question]/page.tsx
index 7823ee0..5e006fe 100644
--- a/app/[chapter]/[subchapter]/questions/[question]/page.tsx
+++ b/app/[chapter]/[subchapter]/questions/[question]/page.tsx
@@ -3,6 +3,8 @@ import { content } from '@/content';
import Link from 'next/link';
import Question from '@/components/question/question';
import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
export default function SubchapterQuestionPage({
params: {
@@ -32,6 +34,24 @@ export default function SubchapterQuestionPage({
const [showSolution, setShowSolution] = useState(false);
+ const explanationMutation = useMutation({
+ mutationKey: ['explanation', questions[questionIndex]],
+ mutationFn: async () => {
+ const response = await fetch('api', {
+ method: 'POST',
+ body: JSON.stringify({
+ question: questions[questionIndex],
+ }),
+ });
+
+ const data = await response.text();
+ console.log(data);
+ return data;
+ },
+ });
+
+ console.log(explanationMutation.isSuccess);
+
return (
<>
@@ -50,6 +70,25 @@ export default function SubchapterQuestionPage({
setShowSolution={setShowSolution}
onAnswerCheck={() => setShowSolution(true)}
/>
+ {explanationMutation.isSuccess ? (
+
+
Explanation
+
+ {explanationMutation.data}
+
+
+ ) : (
+
+ )}
>
diff --git a/app/[chapter]/[subchapter]/questions/api/route.ts b/app/[chapter]/[subchapter]/questions/api/route.ts
new file mode 100644
index 0000000..8ea9e16
--- /dev/null
+++ b/app/[chapter]/[subchapter]/questions/api/route.ts
@@ -0,0 +1,22 @@
+import OpenAI from 'openai';
+
+const openai = new OpenAI();
+
+export async function POST(request: Request) {
+ const body = await request.json();
+
+ const params: OpenAI.Chat.ChatCompletionCreateParams = {
+ messages: [
+ {
+ role: 'system',
+ content: 'Please explain this question using natural language',
+ },
+ { role: 'user', content: JSON.stringify(body.question) },
+ ],
+ model: 'gpt-3.5-turbo',
+ };
+
+ const completion = await openai.chat.completions.create(params);
+
+ return new Response(completion.choices[0].message.content);
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index cf3dca8..8207b31 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,7 +4,7 @@ import './globals.css';
import Navbar from '@/components/navbar';
import { Footer } from '@/components/footer';
import { content } from '@/content';
-import { CSPostHogProvider } from '@/components/providers';
+import { CSPostHogProvider, ReactQueryProvider } from '@/components/providers';
const inter = Inter({
subsets: ['latin'],
@@ -40,13 +40,15 @@ export default function RootLayout({
return (
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
diff --git a/components/providers.tsx b/components/providers.tsx
index 59a2dba..ac63ade 100644
--- a/components/providers.tsx
+++ b/components/providers.tsx
@@ -1,4 +1,5 @@
'use client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { ReactNode } from 'react';
@@ -11,3 +12,10 @@ if (typeof window !== 'undefined') {
export function CSPostHogProvider({ children }: { children: ReactNode }) {
return {children};
}
+
+const queryClient = new QueryClient();
+export function ReactQueryProvider({ children }: { children: ReactNode }) {
+ return (
+ {children}
+ );
+}
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..01ff19c
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/package.json b/package.json
index 00e86ea..a002336 100644
--- a/package.json
+++ b/package.json
@@ -14,16 +14,19 @@
"type": "module",
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@supabase/ssr": "^0.3.0",
"@supabase/supabase-js": "^2.42.3",
+ "@tanstack/react-query": "^5.32.0",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.0",
"lucide-react": "^0.367.0",
"next": "14.1.3",
+ "openai": "^4.38.5",
"posthog-js": "^1.121.4",
"posthog-node": "^4.0.0",
"react": "^18.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 652d4ce..1414b54 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ dependencies:
'@radix-ui/react-accordion':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-dialog':
+ specifier: ^1.0.5
+ version: 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
@@ -23,6 +26,9 @@ dependencies:
'@supabase/supabase-js':
specifier: ^2.42.3
version: 2.42.3
+ '@tanstack/react-query':
+ specifier: ^5.32.0
+ version: 5.32.0(react@18.2.0)
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@@ -38,6 +44,9 @@ dependencies:
next:
specifier: 14.1.3
version: 14.1.3(@babel/core@7.24.4)(react-dom@18.2.0)(react@18.2.0)
+ openai:
+ specifier: ^4.38.5
+ version: 4.38.5
posthog-js:
specifier: ^1.121.4
version: 1.128.1
@@ -935,6 +944,40 @@ packages:
react: 18.2.0
dev: false
+ /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.4
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.78)(react@18.2.0)
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.78)(react@18.2.0)
+ '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.78)(react@18.2.0)
+ '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-id': 1.0.1(@types/react@18.2.78)(react@18.2.0)
+ '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.78)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.2.78)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.78)(react@18.2.0)
+ '@types/react': 18.2.78
+ '@types/react-dom': 18.2.25
+ aria-hidden: 1.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-remove-scroll: 2.5.5(@types/react@18.2.78)(react@18.2.0)
+ dev: false
+
/@radix-ui/react-direction@1.0.1(@types/react@18.2.78)(react@18.2.0):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:
@@ -1553,6 +1596,19 @@ packages:
tslib: 2.6.2
dev: false
+ /@tanstack/query-core@5.32.0:
+ resolution: {integrity: sha512-Z3flEgCat55DRXU5UMwYU1U+DgFZKA3iufyOKs+II7iRAo0uXkeU7PH5e6sOH1CGEag0IpKmZxlUFpCg6roSKw==}
+ dev: false
+
+ /@tanstack/react-query@5.32.0(react@18.2.0):
+ resolution: {integrity: sha512-+E3UudQtarnx9A6xhpgMZapyF+aJfNBGFMgI459FnduEZqT/9KhOWnMOneZahLRt52yzskSA0AuOyLkXHK0yBA==}
+ peerDependencies:
+ react: ^18.0.0
+ dependencies:
+ '@tanstack/query-core': 5.32.0
+ react: 18.2.0
+ dev: false
+
/@testing-library/dom@10.0.0:
resolution: {integrity: sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==}
engines: {node: '>=18'}
@@ -1662,6 +1718,19 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
+ /@types/node-fetch@2.6.11:
+ resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
+ dependencies:
+ '@types/node': 20.12.7
+ form-data: 4.0.0
+ dev: false
+
+ /@types/node@18.19.31:
+ resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==}
+ dependencies:
+ undici-types: 5.26.5
+ dev: false
+
/@types/node@20.12.7:
resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==}
dependencies:
@@ -1886,6 +1955,13 @@ packages:
pretty-format: 29.7.0
dev: true
+ /abort-controller@3.0.0:
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+ engines: {node: '>=6.5'}
+ dependencies:
+ event-target-shim: 5.0.1
+ dev: false
+
/acorn-jsx@5.3.2(acorn@8.11.3):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1914,6 +1990,13 @@ packages:
- supports-color
dev: true
+ /agentkeepalive@4.5.0:
+ resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
+ engines: {node: '>= 8.0.0'}
+ dependencies:
+ humanize-ms: 1.2.1
+ dev: false
+
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
dependencies:
@@ -2992,6 +3075,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /event-target-shim@5.0.1:
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+ engines: {node: '>=6'}
+ dev: false
+
/execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@@ -3095,6 +3183,10 @@ packages:
cross-spawn: 7.0.3
signal-exit: 4.1.0
+ /form-data-encoder@1.7.2:
+ resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
+ dev: false
+
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@@ -3103,6 +3195,14 @@ packages:
combined-stream: 1.0.8
mime-types: 2.1.35
+ /formdata-node@4.4.1:
+ resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
+ engines: {node: '>= 12.20'}
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 4.0.0-beta.3
+ dev: false
+
/fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
@@ -3346,6 +3446,12 @@ packages:
engines: {node: '>=16.17.0'}
dev: true
+ /humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+ dependencies:
+ ms: 2.1.3
+ dev: false
+
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -3892,7 +3998,6 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- dev: true
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -3949,6 +4054,23 @@ packages:
- babel-plugin-macros
dev: false
+ /node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ dev: false
+
+ /node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: false
+
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
@@ -4058,6 +4180,22 @@ packages:
mimic-fn: 4.0.0
dev: true
+ /openai@4.38.5:
+ resolution: {integrity: sha512-Ym5GJL98ZhLJJ7enBx53jjG3vwN/fsB+Ozh46nnRZZS9W1NiYqbwkJ+sXd3dkCIiWIgcyyOPL2Zr8SQAzbpj3g==}
+ hasBin: true
+ dependencies:
+ '@types/node': 18.19.31
+ '@types/node-fetch': 2.6.11
+ abort-controller: 3.0.0
+ agentkeepalive: 4.5.0
+ form-data-encoder: 1.7.2
+ formdata-node: 4.4.1
+ node-fetch: 2.7.0
+ web-streams-polyfill: 3.3.3
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@@ -5343,6 +5481,16 @@ packages:
xml-name-validator: 5.0.0
dev: true
+ /web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+ engines: {node: '>= 8'}
+ dev: false
+
+ /web-streams-polyfill@4.0.0-beta.3:
+ resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
+ engines: {node: '>= 14'}
+ dev: false
+
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false