Skip to content

Commit 488da05

Browse files
committed
feat: adiciona geracao automatica de questoes por texto, audio e documento
2 parents dec133f + 6ba828c commit 488da05

File tree

22 files changed

+2253
-169
lines changed

22 files changed

+2253
-169
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ jobs:
3131
port: 22
3232
script: |
3333
cd /root/Hublast/QuizLab
34-
docker compose up --build -d
34+
docker compose down
35+
docker compose up --build -d

frontend/src/app/(home)/create/page.tsx

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
// /create/page.tsx
12
"use client";
23

34
import { useState } from "react";
45
import { Button } from "@/components/ui/button";
56
import Questions from "./questions/questions";
67
import { useTheme } from "@/hook/useTheme";
78
import { useSubTopic } from "@/hook/useSubTopic";
9+
import { useQuestion} from "@/hook/useQuestion";
10+
import { useQuiz } from "@/hook/useQuiz";
811
import SetQuiz from "./theme/set";
12+
import { AutomaticModeData } from "./questions/automatico/automatico";
13+
import { QuizPergunta } from "@/util/types/quiz";
914

1015
interface ThemeData {
1116
id?: string;
@@ -28,6 +33,8 @@ export interface QuestionData {
2833
correct: boolean;
2934
explanation: string;
3035
}[];
36+
theme_id?: string;
37+
sub_topic_id?: string;
3138
}
3239

3340
type Step = "setup" | "questions";
@@ -37,10 +44,14 @@ export default function CreateQuiz() {
3744
const [themeData, setThemeData] = useState<ThemeData | null>(null);
3845
const [subtopicData, setSubtopicData] = useState<SubtopicData | null>(null);
3946
const [questionsData, setQuestionsData] = useState<QuestionData[]>([]);
47+
const [isAutomatic, setIsAutomatic] = useState(false);
48+
const [automaticData, setAutomaticData] = useState<AutomaticModeData | null>(null);
4049
const [saving, setSaving] = useState(false);
4150

4251
const { createTheme } = useTheme();
4352
const { createSubTopic } = useSubTopic();
53+
const { createQuestion } = useQuestion();
54+
const { createQuizFromText, createQuizFromAudio, createQuizFromDocument } = useQuiz();
4455

4556
const handleNext = () => {
4657
if (!themeData?.title) {
@@ -60,10 +71,39 @@ export default function CreateQuiz() {
6071
setCurrentStep("setup");
6172
};
6273

74+
const convertQuizToQuestions = (quizPerguntas: QuizPergunta[]): QuestionData[] => {
75+
return quizPerguntas.map((pergunta) => ({
76+
text: pergunta.pergunta,
77+
alternatives: pergunta.alternativas.map((alt) => ({
78+
text: alt.texto,
79+
correct: alt.correta,
80+
explanation: alt.explicacao,
81+
})),
82+
theme_id: pergunta.theme_id,
83+
sub_topic_id: pergunta.sub_topic_id,
84+
}));
85+
};
86+
6387
const handleFinish = async () => {
64-
if (questionsData.length === 0) {
65-
alert("Adicione pelo menos uma questão!");
66-
return;
88+
// Validação
89+
if (isAutomatic) {
90+
if (!automaticData) {
91+
alert("Preencha os dados para geração automática!");
92+
return;
93+
}
94+
if (automaticData.mode === "text" && !automaticData.text) {
95+
alert("Preencha o texto para geração!");
96+
return;
97+
}
98+
if ((automaticData.mode === "audio" || automaticData.mode === "document") && !automaticData.file) {
99+
alert("Selecione um arquivo para geração!");
100+
return;
101+
}
102+
} else {
103+
if (questionsData.length === 0) {
104+
alert("Adicione pelo menos uma questão!");
105+
return;
106+
}
67107
}
68108

69109
setSaving(true);
@@ -89,20 +129,58 @@ export default function CreateQuiz() {
89129
subTopicId = newSubTopic.id;
90130
}
91131

92-
// 3. Cria todas as questões
93-
for (const question of questionsData) {
94-
await fetch(`/quiz-lab/api/questions`, {
95-
method: "POST",
96-
headers: {
97-
"Content-Type": "application/json",
98-
"Authorization": `Bearer ${localStorage.getItem("access_token")}`,
99-
},
100-
body: JSON.stringify({
132+
// 3. Gera ou salva as questões
133+
if (isAutomatic && automaticData) {
134+
let generatedQuestions: QuestionData[] = [];
135+
let response;
136+
137+
if (automaticData.mode === "text" && automaticData.text) {
138+
response = await createQuizFromText({
139+
text: automaticData.text,
140+
num_questions: automaticData.num_questions,
141+
num_alternatives: automaticData.num_alternatives,
142+
theme_id: themeId!,
143+
sub_topic_id: subTopicId!
144+
});
145+
} else if (automaticData.mode === "audio" && automaticData.file) {
146+
response = await createQuizFromAudio({
147+
file: automaticData.file,
148+
num_questions: automaticData.num_questions,
149+
num_alternatives: automaticData.num_alternatives,
150+
theme_id: themeId!,
151+
sub_topic_id: subTopicId!
152+
});
153+
} else if (automaticData.mode === "document" && automaticData.file) {
154+
response = await createQuizFromDocument({
155+
file: automaticData.file,
156+
num_questions: automaticData.num_questions,
157+
num_alternatives: automaticData.num_alternatives,
158+
theme_id: themeId!,
159+
sub_topic_id: subTopicId!
160+
});
161+
}
162+
163+
if (response && response.perguntas) {
164+
generatedQuestions = convertQuizToQuestions(response.perguntas);
165+
166+
// Salva as questões geradas no banco
167+
for (const question of generatedQuestions) {
168+
await createQuestion({
169+
text: question.text,
170+
sub_topic_id: subTopicId!,
171+
alternatives: question.alternatives,
172+
});
173+
}
174+
}
175+
} else {
176+
// Salva as questões manuais
177+
for (const question of questionsData) {
178+
await createQuestion({
101179
text: question.text,
102-
sub_topic_id: subTopicId,
180+
sub_topic_id: subTopicId!,
103181
alternatives: question.alternatives,
104-
}),
105-
});
182+
});
183+
}
106184
}
107185

108186
alert("Quiz criado com sucesso!");
@@ -128,7 +206,11 @@ export default function CreateQuiz() {
128206
{currentStep === "questions" && (
129207
<Questions
130208
onQuestionsChange={setQuestionsData}
209+
onAutomaticDataChange={setAutomaticData}
210+
onModeChange={(mode) => setIsAutomatic(mode === "automatic")}
131211
questions={questionsData}
212+
themeId={themeData?.id || ""}
213+
subTopicId={subtopicData?.id || ""}
132214
/>
133215
)}
134216

@@ -147,7 +229,7 @@ export default function CreateQuiz() {
147229

148230
{currentStep === "questions" && (
149231
<Button onClick={handleFinish} disabled={saving} className="ml-auto">
150-
{saving ? "Salvando..." : "Finalizar"}
232+
{saving ? "Processando..." : "Finalizar"}
151233
</Button>
152234
)}
153235
</nav>
Lines changed: 161 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,165 @@
1-
export function Audio() {
1+
// /create/question/automatico/audio.tsx
2+
"use client";
3+
4+
import { useState } from "react";
5+
6+
interface AudioProps {
7+
onDataChange: (data: { file: File; num_questions: number; num_alternatives: number } | null) => void;
8+
}
9+
10+
export function Audio({ onDataChange }: AudioProps) {
11+
const [audioFile, setAudioFile] = useState<File | null>(null);
12+
const [numQuestions, setNumQuestions] = useState(5);
13+
const [numAlternatives, setNumAlternatives] = useState(4);
14+
15+
const updateData = (file: File | null, newNumQuestions: number, newNumAlternatives: number) => {
16+
if (file && newNumQuestions >= 1 && newNumQuestions <= 50 && newNumAlternatives >= 2 && newNumAlternatives <= 6) {
17+
onDataChange({
18+
file: file,
19+
num_questions: newNumQuestions,
20+
num_alternatives: newNumAlternatives
21+
});
22+
} else {
23+
onDataChange(null);
24+
}
25+
};
26+
27+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28+
const file = e.target.files?.[0];
29+
if (file) {
30+
const validTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/m4a'];
31+
if (!validTypes.includes(file.type) && !file.name.match(/\.(mp3|wav|ogg|m4a)$/i)) {
32+
alert("Por favor, selecione um arquivo de áudio válido (MP3, WAV, OGG, M4A).");
33+
return;
34+
}
35+
setAudioFile(file);
36+
updateData(file, numQuestions, numAlternatives);
37+
}
38+
};
39+
40+
const handleRemoveFile = () => {
41+
setAudioFile(null);
42+
updateData(null, numQuestions, numAlternatives);
43+
};
44+
45+
const handleQuestionsChange = (value: number) => {
46+
setNumQuestions(value);
47+
updateData(audioFile, value, numAlternatives);
48+
};
49+
50+
const handleAlternativesChange = (value: number) => {
51+
setNumAlternatives(value);
52+
updateData(audioFile, numQuestions, value);
53+
};
54+
255
return (
3-
<div>
4-
<h2>Gerar questões automaticamente a partir de um áudio</h2>
5-
6-
quantidade de alternartivas por questão
7-
<input
8-
type="number"
9-
/>
10-
numero de questoes
11-
<input
12-
type="number"
13-
/>
14-
<input type="file" accept="audio/*" />
56+
<div className="max-w-4xl mx-auto">
57+
<div className="mb-6">
58+
<h3 className="text-xl font-bold mb-2">Arquivo de Áudio</h3>
59+
<p className="text-gray-600">
60+
Envie um arquivo de áudio que será transcrito e usado como base para gerar as questões automaticamente
61+
</p>
62+
</div>
63+
64+
<div className="bg-layout-card border rounded-lg p-6 mb-6">
65+
<h4 className="font-semibold mb-4">Configurações</h4>
66+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
67+
<div>
68+
<label htmlFor="numQuestions" className="block text-sm font-medium mb-2">
69+
Número de Questões
70+
</label>
71+
<input
72+
id="numQuestions"
73+
type="number"
74+
min="1"
75+
max="50"
76+
value={numQuestions}
77+
onChange={(e) => handleQuestionsChange(Number(e.target.value))}
78+
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all"
79+
/>
80+
<p className="text-xs text-gray-500 mt-1">Entre 1 e 50 questões</p>
81+
</div>
82+
83+
<div>
84+
<label htmlFor="numAlternatives" className="block text-sm font-medium mb-2">
85+
Alternativas por Questão
86+
</label>
87+
<input
88+
id="numAlternatives"
89+
type="number"
90+
min="2"
91+
max="6"
92+
value={numAlternatives}
93+
onChange={(e) => handleAlternativesChange(Number(e.target.value))}
94+
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all"
95+
/>
96+
<p className="text-xs text-gray-500 mt-1">Entre 2 e 6 alternativas</p>
97+
</div>
98+
</div>
99+
</div>
100+
101+
<div className="bg-layout-card border rounded-lg p-6 mb-6">
102+
<label htmlFor="audioInput" className="block text-sm font-medium mb-2">
103+
Arquivo de Áudio
104+
</label>
105+
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-purple-400 transition-colors">
106+
<input
107+
id="audioInput"
108+
type="file"
109+
accept="audio/*,.mp3,.wav,.ogg,.m4a"
110+
onChange={handleFileChange}
111+
className="hidden"
112+
/>
113+
<label htmlFor="audioInput" className="cursor-pointer">
114+
<div className="flex flex-col items-center">
115+
<svg className="w-12 h-12 text-purple-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
117+
</svg>
118+
{audioFile ? (
119+
<div>
120+
<p className="text-sm font-medium text-gray-900">{audioFile.name}</p>
121+
<p className="text-xs text-gray-500 mt-1">
122+
{(audioFile.size / 1024 / 1024).toFixed(2)} MB
123+
</p>
124+
</div>
125+
) : (
126+
<div>
127+
<p className="text-sm font-medium text-gray-900">
128+
Clique para selecionar um arquivo de áudio
129+
</p>
130+
<p className="text-xs text-gray-500 mt-1">
131+
MP3, WAV, OGG ou M4A
132+
</p>
133+
</div>
134+
)}
135+
</div>
136+
</label>
137+
</div>
138+
{audioFile && (
139+
<div className="flex justify-end mt-2">
140+
<button
141+
onClick={handleRemoveFile}
142+
className="text-xs text-red-600 hover:text-red-700"
143+
>
144+
Remover arquivo
145+
</button>
146+
</div>
147+
)}
148+
</div>
149+
150+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
151+
<div className="flex items-start gap-3">
152+
<svg className="w-5 h-5 text-yellow-600 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20">
153+
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
154+
</svg>
155+
<div>
156+
<p className="text-sm font-medium text-yellow-900">Importante</p>
157+
<p className="text-sm text-yellow-800">
158+
O áudio será transcrito automaticamente. Para melhores resultados, use áudios com boa qualidade de som e fala clara. Clique em "Finalizar" para criar o quiz.
159+
</p>
160+
</div>
161+
</div>
162+
</div>
15163
</div>
16164
);
17165
}

0 commit comments

Comments
 (0)