speaking and questions content filtering
This commit is contained in:
parent
6910fb55a4
commit
21a23d9a88
|
|
@ -33,6 +33,7 @@ import type {
|
||||||
GetQuestionSetsParams,
|
GetQuestionSetsParams,
|
||||||
GetQuestionSetDetailResponse,
|
GetQuestionSetDetailResponse,
|
||||||
GetQuestionSetQuestionsResponse,
|
GetQuestionSetQuestionsResponse,
|
||||||
|
GetPracticeQuestionsByPracticeResponse,
|
||||||
CreateQuestionSetRequest,
|
CreateQuestionSetRequest,
|
||||||
CreateQuestionSetResponse,
|
CreateQuestionSetResponse,
|
||||||
AddQuestionToSetRequest,
|
AddQuestionToSetRequest,
|
||||||
|
|
@ -147,6 +148,14 @@ export const deletePractice = (practiceId: number) =>
|
||||||
export const getPracticeQuestions = (practiceId: number) =>
|
export const getPracticeQuestions = (practiceId: number) =>
|
||||||
http.get<GetQuestionSetQuestionsResponse>(`/question-sets/${practiceId}/questions`)
|
http.get<GetQuestionSetQuestionsResponse>(`/question-sets/${practiceId}/questions`)
|
||||||
|
|
||||||
|
export const getPracticeQuestionsByPractice = (
|
||||||
|
practiceId: number,
|
||||||
|
params?: { limit?: number; offset?: number },
|
||||||
|
) =>
|
||||||
|
http.get<GetPracticeQuestionsByPracticeResponse>(`/practices/${practiceId}/questions`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
|
||||||
export const createPracticeQuestion = (data: CreatePracticeQuestionRequest) =>
|
export const createPracticeQuestion = (data: CreatePracticeQuestionRequest) =>
|
||||||
http.post("/course-management/practice-questions", data)
|
http.post("/course-management/practice-questions", data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import { Link, useParams } from "react-router-dom"
|
import { Link, useParams } from "react-router-dom"
|
||||||
import { ArrowLeft, Plus, Edit, Trash2, X, Check, ChevronDown, ChevronUp, SlidersHorizontal, ArrowUpDown } from "lucide-react"
|
import { ArrowLeft, Plus, Edit, Trash2, X, Check, ChevronDown, ChevronUp, SlidersHorizontal, ArrowUpDown } from "lucide-react"
|
||||||
import practiceSrc from "../../assets/Practice.svg"
|
import practiceSrc from "../../assets/Practice.svg"
|
||||||
|
|
@ -9,7 +9,7 @@ import { Badge } from "../../components/ui/badge"
|
||||||
import { Button } from "../../components/ui/button"
|
import { Button } from "../../components/ui/button"
|
||||||
import {
|
import {
|
||||||
getQuestionSetById,
|
getQuestionSetById,
|
||||||
getQuestionSetQuestions,
|
getPracticeQuestionsByPractice,
|
||||||
getQuestionById,
|
getQuestionById,
|
||||||
deleteQuestion,
|
deleteQuestion,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
|
|
@ -21,7 +21,7 @@ import { Select } from "../../components/ui/select"
|
||||||
import { Textarea } from "../../components/ui/textarea"
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
import type { PracticeQuestion, QuestionSetQuestion, QuestionDetail } from "../../types/course.types"
|
import type { PracticeQuestion, QuestionSetQuestion, QuestionDetail } from "../../types/course.types"
|
||||||
|
|
||||||
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT"
|
type QuestionType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO"
|
||||||
type DifficultyLevel = "EASY" | "MEDIUM" | "HARD"
|
type DifficultyLevel = "EASY" | "MEDIUM" | "HARD"
|
||||||
type GroupByOption = "none" | "type" | "difficulty"
|
type GroupByOption = "none" | "type" | "difficulty"
|
||||||
type PointsSortOption = "asc" | "desc"
|
type PointsSortOption = "asc" | "desc"
|
||||||
|
|
@ -47,12 +47,14 @@ const typeLabels: Record<QuestionType, string> = {
|
||||||
MCQ: "Multiple Choice",
|
MCQ: "Multiple Choice",
|
||||||
TRUE_FALSE: "True/False",
|
TRUE_FALSE: "True/False",
|
||||||
SHORT: "Short Answer",
|
SHORT: "Short Answer",
|
||||||
|
AUDIO: "Audio",
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeColors: Record<QuestionType, string> = {
|
const typeColors: Record<QuestionType, string> = {
|
||||||
MCQ: "bg-blue-100 text-blue-700",
|
MCQ: "bg-blue-100 text-blue-700",
|
||||||
TRUE_FALSE: "bg-purple-100 text-purple-700",
|
TRUE_FALSE: "bg-purple-100 text-purple-700",
|
||||||
SHORT: "bg-green-100 text-green-700",
|
SHORT: "bg-green-100 text-green-700",
|
||||||
|
AUDIO: "bg-brand-100 text-brand-700",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PracticeQuestionsPage() {
|
export function PracticeQuestionsPage() {
|
||||||
|
|
@ -75,6 +77,9 @@ export function PracticeQuestionsPage() {
|
||||||
const [loadingDetailIds, setLoadingDetailIds] = useState<Record<number, boolean>>({})
|
const [loadingDetailIds, setLoadingDetailIds] = useState<Record<number, boolean>>({})
|
||||||
const [groupBy, setGroupBy] = useState<GroupByOption>("none")
|
const [groupBy, setGroupBy] = useState<GroupByOption>("none")
|
||||||
const [pointsSort, setPointsSort] = useState<PointsSortOption>("desc")
|
const [pointsSort, setPointsSort] = useState<PointsSortOption>("desc")
|
||||||
|
const [pageSize] = useState(10)
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [totalQuestions, setTotalQuestions] = useState(0)
|
||||||
|
|
||||||
const [draft, setDraft] = useState<QuestionDraft>({
|
const [draft, setDraft] = useState<QuestionDraft>({
|
||||||
questionText: "",
|
questionText: "",
|
||||||
|
|
@ -194,19 +199,22 @@ export function PracticeQuestionsPage() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchQuestions = async () => {
|
const fetchQuestions = useCallback(async (page: number = currentPage) => {
|
||||||
if (!practiceId) return
|
if (!practiceId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const safePage = page < 1 ? 1 : page
|
||||||
|
const offset = (safePage - 1) * pageSize
|
||||||
const [detailRes, questionsRes] = await Promise.all([
|
const [detailRes, questionsRes] = await Promise.all([
|
||||||
getQuestionSetById(Number(practiceId)),
|
getQuestionSetById(Number(practiceId)),
|
||||||
getQuestionSetQuestions(Number(practiceId)),
|
getPracticeQuestionsByPractice(Number(practiceId), { limit: pageSize, offset }),
|
||||||
])
|
])
|
||||||
const detail = detailRes.data?.data
|
const detail = detailRes.data?.data
|
||||||
setPracticeTitle(detail?.title || "Practice Questions")
|
setPracticeTitle(detail?.title || "Practice Questions")
|
||||||
setPracticeDescription(detail?.description || "")
|
setPracticeDescription(detail?.description || "")
|
||||||
|
|
||||||
const mappedQuestions: PracticeQuestion[] = (questionsRes.data?.data ?? []).map(
|
const payload = questionsRes.data?.data
|
||||||
|
const mappedQuestions: PracticeQuestion[] = (payload?.questions ?? []).map(
|
||||||
(question: QuestionSetQuestion) => ({
|
(question: QuestionSetQuestion) => ({
|
||||||
id: question.question_id || question.id,
|
id: question.question_id || question.id,
|
||||||
practice_id: question.set_id,
|
practice_id: question.set_id,
|
||||||
|
|
@ -214,13 +222,14 @@ export function PracticeQuestionsPage() {
|
||||||
points: question.points ?? 0,
|
points: question.points ?? 0,
|
||||||
difficulty_level: question.difficulty_level || "",
|
difficulty_level: question.difficulty_level || "",
|
||||||
question_voice_prompt: question.voice_prompt || "",
|
question_voice_prompt: question.voice_prompt || "",
|
||||||
sample_answer_voice_prompt: "",
|
sample_answer_voice_prompt: question.sample_answer_voice_prompt || "",
|
||||||
sample_answer: question.explanation || "",
|
sample_answer: question.audio_correct_answer_text || question.explanation || "",
|
||||||
tips: question.tips || "",
|
tips: question.tips || "",
|
||||||
type:
|
type:
|
||||||
question.question_type === "MCQ" ||
|
question.question_type === "MCQ" ||
|
||||||
question.question_type === "TRUE_FALSE" ||
|
question.question_type === "TRUE_FALSE" ||
|
||||||
question.question_type === "SHORT"
|
question.question_type === "SHORT" ||
|
||||||
|
question.question_type === "AUDIO"
|
||||||
? question.question_type
|
? question.question_type
|
||||||
: question.question_type === "SHORT_ANSWER"
|
: question.question_type === "SHORT_ANSWER"
|
||||||
? "SHORT"
|
? "SHORT"
|
||||||
|
|
@ -228,17 +237,19 @@ export function PracticeQuestionsPage() {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
setQuestions(mappedQuestions)
|
setQuestions(mappedQuestions)
|
||||||
|
setTotalQuestions(payload?.total_count ?? mappedQuestions.length)
|
||||||
|
setCurrentPage(safePage)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch questions:", err)
|
console.error("Failed to fetch questions:", err)
|
||||||
setError("Failed to load questions")
|
setError("Failed to load questions")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [practiceId, currentPage, pageSize])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchQuestions()
|
fetchQuestions()
|
||||||
}, [practiceId])
|
}, [fetchQuestions])
|
||||||
|
|
||||||
const handleAddQuestion = () => {
|
const handleAddQuestion = () => {
|
||||||
resetDraft()
|
resetDraft()
|
||||||
|
|
@ -525,7 +536,10 @@ export function PracticeQuestionsPage() {
|
||||||
{practiceDescription && (
|
{practiceDescription && (
|
||||||
<p className="mt-0.5 text-sm text-grayScale-500">{practiceDescription}</p>
|
<p className="mt-0.5 text-sm text-grayScale-500">{practiceDescription}</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-0.5 text-sm text-grayScale-500">{questions.length} questions available</p>
|
<p className="mt-0.5 text-sm text-grayScale-500">{questions.length} questions on this page</p>
|
||||||
|
<p className="mt-0.5 text-xs text-grayScale-400">
|
||||||
|
Total: {totalQuestions} question{totalQuestions === 1 ? "" : "s"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-brand-500 hover:bg-brand-600" onClick={handleAddQuestion}>
|
<Button className="bg-brand-500 hover:bg-brand-600" onClick={handleAddQuestion}>
|
||||||
|
|
@ -707,6 +721,31 @@ export function PracticeQuestionsPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{totalQuestions > pageSize && (
|
||||||
|
<div className="flex items-center justify-between rounded-xl border border-grayScale-200 bg-white px-4 py-3">
|
||||||
|
<p className="text-sm text-grayScale-500">
|
||||||
|
Page {currentPage} of {Math.max(1, Math.ceil(totalQuestions / pageSize))}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={currentPage <= 1}
|
||||||
|
onClick={() => void fetchQuestions(currentPage - 1)}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={currentPage >= Math.ceil(totalQuestions / pageSize)}
|
||||||
|
onClick={() => void fetchQuestions(currentPage + 1)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,9 @@ function introVideoUrlFromUploadResponse(data: { url?: string; embed_url?: strin
|
||||||
|
|
||||||
export function SpeakingPage() {
|
export function SpeakingPage() {
|
||||||
const [audioQuestions, setAudioQuestions] = useState<QuestionDetail[]>([])
|
const [audioQuestions, setAudioQuestions] = useState<QuestionDetail[]>([])
|
||||||
|
const [audioTotalCount, setAudioTotalCount] = useState(0)
|
||||||
|
const [audioPage, setAudioPage] = useState(1)
|
||||||
|
const [audioPageSize] = useState(12)
|
||||||
const [audioPreviewByQuestionId, setAudioPreviewByQuestionId] = useState<Record<number, string>>({})
|
const [audioPreviewByQuestionId, setAudioPreviewByQuestionId] = useState<Record<number, string>>({})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
@ -139,7 +142,7 @@ export function SpeakingPage() {
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{ id: number; text: string } | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<{ id: number; text: string } | null>(null)
|
||||||
const [detailForm, setDetailForm] = useState({
|
const [detailForm, setDetailForm] = useState({
|
||||||
question_text: "",
|
question_text: "",
|
||||||
question_type: "AUDIO" as "AUDIO",
|
question_type: "AUDIO" as const,
|
||||||
difficulty_level: "EASY" as "EASY" | "MEDIUM" | "HARD",
|
difficulty_level: "EASY" as "EASY" | "MEDIUM" | "HARD",
|
||||||
points: 1,
|
points: 1,
|
||||||
explanation: "",
|
explanation: "",
|
||||||
|
|
@ -187,59 +190,49 @@ export function SpeakingPage() {
|
||||||
return res.data?.data?.url ?? ""
|
return res.data?.data?.url ?? ""
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchAudioQuestions = useCallback(async () => {
|
const fetchAudioQuestions = useCallback(async (page: number = audioPage) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const batchSize = 100
|
const safePage = page < 1 ? 1 : page
|
||||||
let nextOffset = 0
|
const offset = (safePage - 1) * audioPageSize
|
||||||
let expectedTotal = Number.POSITIVE_INFINITY
|
const res = await getQuestions({
|
||||||
let allRows: QuestionDetail[] = []
|
question_type: "AUDIO",
|
||||||
|
limit: audioPageSize,
|
||||||
|
offset,
|
||||||
|
})
|
||||||
|
const payload = res.data?.data as unknown
|
||||||
|
const meta = res.data?.metadata as { total_count?: number } | null | undefined
|
||||||
|
|
||||||
while (allRows.length < expectedTotal) {
|
let rows: QuestionDetail[] = []
|
||||||
const res = await getQuestions({
|
let total = 0
|
||||||
question_type: "AUDIO",
|
if (Array.isArray(payload)) {
|
||||||
limit: batchSize,
|
rows = payload as QuestionDetail[]
|
||||||
offset: nextOffset,
|
total = meta?.total_count ?? rows.length
|
||||||
})
|
} else if (
|
||||||
const payload = res.data?.data as unknown
|
payload &&
|
||||||
const meta = res.data?.metadata as { total_count?: number } | null | undefined
|
typeof payload === "object" &&
|
||||||
|
Array.isArray((payload as { questions?: unknown[] }).questions)
|
||||||
let chunk: QuestionDetail[] = []
|
) {
|
||||||
let chunkTotal: number | undefined
|
const data = payload as { questions: QuestionDetail[]; total_count?: number }
|
||||||
if (Array.isArray(payload)) {
|
rows = data.questions
|
||||||
chunk = payload as QuestionDetail[]
|
total = data.total_count ?? meta?.total_count ?? rows.length
|
||||||
chunkTotal = meta?.total_count
|
|
||||||
} else if (
|
|
||||||
payload &&
|
|
||||||
typeof payload === "object" &&
|
|
||||||
Array.isArray((payload as { questions?: unknown[] }).questions)
|
|
||||||
) {
|
|
||||||
const data = payload as { questions: QuestionDetail[]; total_count?: number }
|
|
||||||
chunk = data.questions
|
|
||||||
chunkTotal = data.total_count ?? meta?.total_count
|
|
||||||
}
|
|
||||||
|
|
||||||
allRows = [...allRows, ...chunk]
|
|
||||||
if (typeof chunkTotal === "number" && Number.isFinite(chunkTotal)) {
|
|
||||||
expectedTotal = chunkTotal
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.length < batchSize) break
|
|
||||||
nextOffset += chunk.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAudioQuestions(allRows)
|
setAudioQuestions(rows)
|
||||||
|
setAudioTotalCount(total)
|
||||||
|
setAudioPage(safePage)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch audio questions:", error)
|
console.error("Failed to fetch audio questions:", error)
|
||||||
setAudioQuestions([])
|
setAudioQuestions([])
|
||||||
|
setAudioTotalCount(0)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [audioPage, audioPageSize])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAudioQuestions()
|
fetchAudioQuestions()
|
||||||
}, [fetchAudioQuestions])
|
}, [fetchAudioQuestions, audioPageSize])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
@ -988,6 +981,9 @@ export function SpeakingPage() {
|
||||||
<p className="mt-1 text-xs font-normal text-grayScale-500 sm:text-sm">
|
<p className="mt-1 text-xs font-normal text-grayScale-500 sm:text-sm">
|
||||||
Tap a row to view details. Speaking practices create AUDIO question sets linked to a sub-course.
|
Tap a row to view details. Speaking practices create AUDIO question sets linked to a sub-course.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-xs font-normal text-grayScale-400 sm:text-sm">
|
||||||
|
Showing page {audioPage} of {Math.max(1, Math.ceil(audioTotalCount / audioPageSize))} ({audioTotalCount} total)
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pb-6 pt-5 sm:px-6">
|
<CardContent className="px-4 pb-6 pt-5 sm:px-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -1060,6 +1056,33 @@ export function SpeakingPage() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{audioTotalCount > audioPageSize ? (
|
||||||
|
<div className="mt-4 flex items-center justify-between rounded-xl border border-grayScale-200 bg-white px-3 py-2">
|
||||||
|
<p className="text-xs text-grayScale-500 sm:text-sm">
|
||||||
|
Page {audioPage} of {Math.max(1, Math.ceil(audioTotalCount / audioPageSize))}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={audioPage <= 1 || loading}
|
||||||
|
onClick={() => fetchAudioQuestions(audioPage - 1)}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={audioPage >= Math.ceil(audioTotalCount / audioPageSize) || loading}
|
||||||
|
onClick={() => fetchAudioQuestions(audioPage + 1)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ export interface PracticeQuestion {
|
||||||
sample_answer_voice_prompt: string
|
sample_answer_voice_prompt: string
|
||||||
sample_answer: string
|
sample_answer: string
|
||||||
tips: string
|
tips: string
|
||||||
type: "MCQ" | "TRUE_FALSE" | "SHORT"
|
type: "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetPracticeQuestionsResponse {
|
export interface GetPracticeQuestionsResponse {
|
||||||
|
|
@ -455,12 +455,15 @@ export interface QuestionSetQuestion {
|
||||||
question_id: number
|
question_id: number
|
||||||
display_order: number
|
display_order: number
|
||||||
question_text: string
|
question_text: string
|
||||||
question_type: "MCQ" | "TRUE_FALSE" | "SHORT" | string
|
question_type: "MCQ" | "TRUE_FALSE" | "SHORT" | "SHORT_ANSWER" | "AUDIO" | string
|
||||||
difficulty_level?: string | null
|
difficulty_level?: string | null
|
||||||
points?: number
|
points?: number
|
||||||
explanation?: string | null
|
explanation?: string | null
|
||||||
tips?: string | null
|
tips?: string | null
|
||||||
voice_prompt?: string | null
|
voice_prompt?: string | null
|
||||||
|
sample_answer_voice_prompt?: string | null
|
||||||
|
image_url?: string | null
|
||||||
|
audio_correct_answer_text?: string | null
|
||||||
question_status?: string
|
question_status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,6 +475,19 @@ export interface GetQuestionSetQuestionsResponse {
|
||||||
metadata: unknown
|
metadata: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetPracticeQuestionsByPracticeResponse {
|
||||||
|
message: string
|
||||||
|
data: {
|
||||||
|
questions: QuestionSetQuestion[]
|
||||||
|
total_count: number
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
success: boolean
|
||||||
|
status_code: number
|
||||||
|
metadata: unknown
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /question-sets — practices use set_type: "PRACTICE", owner_type: "SUB_COURSE", owner_id: sub-course id */
|
/** POST /question-sets — practices use set_type: "PRACTICE", owner_type: "SUB_COURSE", owner_id: sub-course id */
|
||||||
export interface CreateQuestionSetRequest {
|
export interface CreateQuestionSetRequest {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user