feat: fix frontend and activate imports
This commit is contained in:
@@ -23,15 +23,15 @@ export default function AdminDashboard() {
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
// In a real app we'd have a specific stats endpoint,
|
||||
// In a real app we'd have a specific stats endpoint,
|
||||
// but for now we'll calculate from lists
|
||||
const [orgs, users] = await Promise.all([
|
||||
cmsApi.getOrganizations(),
|
||||
const [org, users] = await Promise.all([
|
||||
cmsApi.getOrganization(),
|
||||
cmsApi.getAllUsers()
|
||||
]);
|
||||
|
||||
setStats({
|
||||
orgs: orgs.length,
|
||||
orgs: 1, // Single tenant architecture
|
||||
users: users.length,
|
||||
courses: 0 // We'd need a global courses count
|
||||
});
|
||||
|
||||
@@ -29,12 +29,12 @@ export default function UsersPage() {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [usersData, orgsData] = await Promise.all([
|
||||
const [usersData, orgData] = await Promise.all([
|
||||
cmsApi.getAllUsers(),
|
||||
cmsApi.getOrganizations()
|
||||
cmsApi.getOrganization()
|
||||
]);
|
||||
setUsers(usersData);
|
||||
setOrganizations(orgsData);
|
||||
setOrganizations([orgData]); // Single tenant - wrap in array for compatibility
|
||||
} catch (error) {
|
||||
console.error('Failed to load data', error);
|
||||
} finally {
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { questionBankApi, QuestionBank, QuestionBankFilters, QuestionBankType } from '@/lib/api';
|
||||
import {
|
||||
Plus, Search, Filter, Edit2, Trash2, Volume2, VolumeX, Download,
|
||||
import {
|
||||
Plus, Search, Filter, Edit2, Trash2, Download,
|
||||
Upload, Sparkles, ChevronDown, ChevronUp, X, Check, AlertCircle,
|
||||
Headphones, BookOpen, Tag, Hash, Globe
|
||||
} from 'lucide-react';
|
||||
import QuestionBankEditor from '@/components/QuestionBank/QuestionBankEditor';
|
||||
import QuestionBankCard from '@/components/QuestionBank/QuestionBankCard';
|
||||
import MySQLImportModal from '@/components/QuestionBank/MySQLImportModal';
|
||||
import AudioGeneratorModal from '@/components/QuestionBank/AudioGeneratorModal';
|
||||
|
||||
export default function QuestionBankPage() {
|
||||
const [questions, setQuestions] = useState<QuestionBank[]>([]);
|
||||
@@ -21,8 +20,6 @@ export default function QuestionBankPage() {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [editingQuestion, setEditingQuestion] = useState<QuestionBank | null>(null);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
||||
const [selectedForAudio, setSelectedForAudio] = useState<string | null>(null);
|
||||
|
||||
const loadQuestions = async () => {
|
||||
try {
|
||||
@@ -67,17 +64,6 @@ export default function QuestionBankPage() {
|
||||
await loadQuestions();
|
||||
};
|
||||
|
||||
const handleAudioGenerate = (questionId: string) => {
|
||||
setSelectedForAudio(questionId);
|
||||
setShowAudioModal(true);
|
||||
};
|
||||
|
||||
const handleAudioSuccess = async () => {
|
||||
setShowAudioModal(false);
|
||||
setSelectedForAudio(null);
|
||||
await loadQuestions();
|
||||
};
|
||||
|
||||
const getQuestionTypeLabel = (type: QuestionBankType) => {
|
||||
const labels: Record<QuestionBankType, string> = {
|
||||
'multiple-choice': 'Opción Múltiple',
|
||||
@@ -311,7 +297,6 @@ export default function QuestionBankPage() {
|
||||
question={question}
|
||||
onEdit={() => handleEdit(question)}
|
||||
onDelete={() => handleDelete(question.id)}
|
||||
onGenerateAudio={() => handleAudioGenerate(question.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -337,18 +322,6 @@ export default function QuestionBankPage() {
|
||||
onCancel={() => setShowImportModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Audio Generator Modal */}
|
||||
{showAudioModal && selectedForAudio && (
|
||||
<AudioGeneratorModal
|
||||
questionId={selectedForAudio}
|
||||
onSuccess={handleAudioSuccess}
|
||||
onCancel={() => {
|
||||
setShowAudioModal(false);
|
||||
setSelectedForAudio(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { questionBankApi, QuestionBank } from '@/lib/api';
|
||||
import { X, Volume2, Check, AlertCircle, Play, Pause } from 'lucide-react';
|
||||
|
||||
interface AudioGeneratorModalProps {
|
||||
questionId: string;
|
||||
onSuccess?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export default function AudioGeneratorModal({ questionId, onSuccess, onCancel }: AudioGeneratorModalProps) {
|
||||
const [question, setQuestion] = useState<QuestionBank | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [generated, setGenerated] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||
|
||||
const [voice, setVoice] = useState('v2/en_speaker_1');
|
||||
const [speed, setSpeed] = useState(1.0);
|
||||
const [customText, setCustomText] = useState('');
|
||||
|
||||
const voices = [
|
||||
{ id: 'v2/en_speaker_0', name: 'English Speaker 0', language: 'en' },
|
||||
{ id: 'v2/en_speaker_1', name: 'English Speaker 1', language: 'en' },
|
||||
{ id: 'v2/en_speaker_6', name: 'English Speaker 6', language: 'en' },
|
||||
{ id: 'v2/es_speaker_0', name: 'Spanish Speaker 0', language: 'es' },
|
||||
{ id: 'v2/es_speaker_1', name: 'Spanish Speaker 1', language: 'es' },
|
||||
{ id: 'v2/es_speaker_3', name: 'Spanish Speaker 3', language: 'es' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadQuestion();
|
||||
}, [questionId]);
|
||||
|
||||
const loadQuestion = async () => {
|
||||
try {
|
||||
const data = await questionBankApi.get(questionId);
|
||||
setQuestion(data);
|
||||
setCustomText(data.question_text);
|
||||
} catch (error) {
|
||||
console.error('Failed to load question:', error);
|
||||
setError('No se pudo cargar la pregunta');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
|
||||
await questionBankApi.generateAudio(questionId, customText, voice, speed);
|
||||
|
||||
// Poll for completion
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // 30 seconds max
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const updated = await questionBankApi.get(questionId);
|
||||
|
||||
if (updated.audio_status === 'ready') {
|
||||
setGenerated(true);
|
||||
setQuestion(updated);
|
||||
break;
|
||||
} else if (updated.audio_status === 'failed') {
|
||||
setError('Error al generar el audio');
|
||||
break;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
setError('Tiempo de espera agotado. El audio puede estar generándose aún.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Audio generation failed:', error);
|
||||
setError(error.message || 'Error al generar audio');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
if (!question?.audio_url) return;
|
||||
|
||||
if (audio) {
|
||||
audio.remove();
|
||||
}
|
||||
|
||||
const audioEl = new Audio(question.audio_url);
|
||||
audioEl.onended = () => setIsPlaying(false);
|
||||
audioEl.onerror = () => {
|
||||
setIsPlaying(false);
|
||||
alert('Error al reproducir el audio');
|
||||
};
|
||||
|
||||
setAudio(audioEl);
|
||||
setIsPlaying(true);
|
||||
audioEl.play();
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-lg w-full p-8 text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Cargando pregunta...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-2xl w-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<Volume2 className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Generar Audio con Bark
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Convierte el texto de la pregunta a audio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Question Preview */}
|
||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Texto de la pregunta
|
||||
</label>
|
||||
<p className="text-gray-900 dark:text-white text-sm">
|
||||
{question?.question_text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom Text */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Texto para audio (opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={customText}
|
||||
onChange={(e) => setCustomText(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Deja en blanco para usar el texto de la pregunta"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Puedes personalizar el texto si quieres una pronunciación diferente
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Voice Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Voz
|
||||
</label>
|
||||
<select
|
||||
value={voice}
|
||||
onChange={(e) => setVoice(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{voices.map((v) => (
|
||||
<option key={v.id} value={v.id}>
|
||||
{v.name} ({v.language === 'en' ? 'Inglés' : 'Español'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Speed */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Velocidad
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={speed}
|
||||
onChange={(e) => setSpeed(parseFloat(e.target.value))}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 w-12 text-right">
|
||||
{speed.toFixed(1)}x
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audio Preview */}
|
||||
{question?.audio_status === 'ready' && question.audio_url && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/40 rounded-full flex items-center justify-center">
|
||||
<Volume2 className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-900 dark:text-green-100">
|
||||
Audio generado
|
||||
</p>
|
||||
<p className="text-xs text-green-700 dark:text-green-300">
|
||||
Haz click para escuchar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={isPlaying ? handleStop : handlePlay}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
{isPlaying ? 'Detener' : 'Reproducir'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generating Status */}
|
||||
{generating && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
Generando audio...
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
Esto puede tomar unos segundos
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-900 dark:text-red-100">
|
||||
Error
|
||||
</p>
|
||||
<p className="text-xs text-red-700 dark:text-red-300">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Message */}
|
||||
{generated && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 flex items-center gap-3">
|
||||
<Check className="w-5 h-5 text-green-600" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-900 dark:text-green-100">
|
||||
¡Audio generado exitosamente!
|
||||
</p>
|
||||
<p className="text-xs text-green-700 dark:text-green-300">
|
||||
El audio está disponible para los estudiantes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={generating}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{generated ? 'Cerrar' : 'Cancelar'}
|
||||
</button>
|
||||
{!question?.audio_url && (
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating || generated}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
<Volume2 className="w-4 h-4" />
|
||||
{generating ? 'Generando...' : 'Generar Audio'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Download, Database, Check, AlertCircle, Upload, FileSpreadsheet } from 'lucide-react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import ExcelImportModal from './ExcelImportModal';
|
||||
|
||||
interface MySQLImportModalProps {
|
||||
@@ -23,17 +24,13 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
||||
try {
|
||||
setImporting(true);
|
||||
setError(null);
|
||||
|
||||
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/question-bank/import-mysql-all`, {
|
||||
|
||||
const result = await apiFetch('/question-bank/import-mysql-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(r => r.json());
|
||||
|
||||
});
|
||||
|
||||
setImportResult(result);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
onSuccess?.();
|
||||
}, 1500);
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { QuestionBank } from '@/lib/api';
|
||||
import { Edit2, Trash2, Volume2, VolumeX, Sparkles, Globe, MoreVertical, Play, Pause } from 'lucide-react';
|
||||
import { Edit2, Trash2, Volume2, Sparkles, Globe } from 'lucide-react';
|
||||
|
||||
interface QuestionBankCardProps {
|
||||
question: QuestionBank;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onGenerateAudio: () => void;
|
||||
}
|
||||
|
||||
export default function QuestionBankCard({ question, onEdit, onDelete, onGenerateAudio }: QuestionBankCardProps) {
|
||||
export default function QuestionBankCard({ question, onEdit, onDelete }: QuestionBankCardProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||
|
||||
@@ -87,21 +86,13 @@ export default function QuestionBankCard({ question, onEdit, onDelete, onGenerat
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{question.audio_status === 'ready' ? (
|
||||
{question.audio_url && (
|
||||
<button
|
||||
onClick={isPlaying ? handleStopAudio : handlePlayAudio}
|
||||
className="p-1.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded transition-colors"
|
||||
title={isPlaying ? 'Detener audio' : 'Reproducir audio'}
|
||||
>
|
||||
{isPlaying ? <Pause className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onGenerateAudio}
|
||||
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
|
||||
title="Generar audio"
|
||||
>
|
||||
<VolumeX className="w-4 h-4" />
|
||||
<Volume2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -175,14 +166,6 @@ export default function QuestionBankCard({ question, onEdit, onDelete, onGenerat
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{question.audio_status === 'ready' && (
|
||||
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<Volume2 className="w-3 h-3" /> Audio listo
|
||||
</span>
|
||||
)}
|
||||
{question.audio_status === 'generating' && (
|
||||
<span className="text-xs text-yellow-600 dark:text-yellow-400">Generando audio...</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Usage Stats */}
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
||||
tags: question?.tags || [],
|
||||
media_url: question?.media_url,
|
||||
media_type: question?.media_type,
|
||||
generate_audio: false,
|
||||
skill_assessed: question?.skill_assessed,
|
||||
});
|
||||
|
||||
@@ -499,21 +498,6 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio Generation Option */}
|
||||
<div className="flex items-center gap-3 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="generate_audio"
|
||||
checked={formData.generate_audio}
|
||||
onChange={(e) => setFormData({ ...formData, generate_audio: e.target.checked })}
|
||||
className="w-4 h-4 text-green-600 rounded"
|
||||
/>
|
||||
<label htmlFor="generate_audio" className="flex items-center gap-2 text-sm text-green-800 dark:text-green-200">
|
||||
<Volume2 className="w-4 h-4" />
|
||||
Generar audio automáticamente con Bark después de guardar
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700 sticky bottom-0 bg-white dark:bg-gray-800 -mx-6 px-6 py-4">
|
||||
<button
|
||||
|
||||
@@ -594,7 +594,7 @@ interface ApiFetchOptions extends RequestInit {
|
||||
query?: Record<string, string | number | boolean | undefined | null>;
|
||||
}
|
||||
|
||||
const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: boolean = false) => {
|
||||
export const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: boolean = false) => {
|
||||
const token = getToken();
|
||||
const selectedOrgId = getSelectedOrgId();
|
||||
const baseUrl = isLms ? LMS_API_BASE_URL : API_BASE_URL;
|
||||
@@ -654,6 +654,7 @@ export const cmsApi = {
|
||||
},
|
||||
getSSOConfig: (): Promise<OrganizationSSOConfig> => apiFetch('/organization/sso'),
|
||||
updateSSOConfig: (payload: Partial<OrganizationSSOConfig>): Promise<void> => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }),
|
||||
getOrganization: (): Promise<Organization> => apiFetch('/organization'),
|
||||
|
||||
// Auth
|
||||
register: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
@@ -980,7 +981,6 @@ export interface CreateQuestionBankPayload {
|
||||
tags?: string[];
|
||||
media_url?: string;
|
||||
media_type?: string;
|
||||
generate_audio?: boolean;
|
||||
skill_assessed?: string;
|
||||
audio_url?: string;
|
||||
audio_text?: string;
|
||||
@@ -1015,8 +1015,6 @@ export const questionBankApi = {
|
||||
apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false),
|
||||
importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise<QuestionBank[]> =>
|
||||
apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false),
|
||||
generateAudio: (id: string, text?: string, voice?: string, speed?: number): Promise<void> =>
|
||||
apiFetch(`/question-bank/${id}/generate-audio`, { method: 'POST', body: JSON.stringify({ text, voice, speed }) }, false),
|
||||
};
|
||||
|
||||
export const lmsApi = {
|
||||
|
||||
Reference in New Issue
Block a user