364 lines
20 KiB
TypeScript
364 lines
20 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { AiOutlineUnorderedList, AiOutlinePlus, AiOutlineDelete } from 'react-icons/ai';
|
|
import Header from "../../Shared/Header";
|
|
import GenLoader from "../Shared/GenLoader";
|
|
import useSectionEdit from "../../Hooks/useSectionEdit";
|
|
import useExamEditorStore from "@/stores/examEditor";
|
|
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
|
import { BsFileText } from "react-icons/bs";
|
|
import { RiVideoLine } from 'react-icons/ri';
|
|
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6';
|
|
|
|
interface Props {
|
|
sectionId: number;
|
|
exercise: InteractiveSpeakingExercise;
|
|
}
|
|
|
|
const Speaking1: React.FC<Props> = ({ sectionId, exercise }) => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const [local, setLocal] = useState(() => {
|
|
const defaultPrompts = [
|
|
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" },
|
|
{ text: "Do you work or do you study?", video_url: "" },
|
|
...exercise.prompts.slice(2)
|
|
];
|
|
return { ...exercise, prompts: defaultPrompts };
|
|
});
|
|
|
|
const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
|
|
|
|
const { generating, genResult } = useExamEditorStore(
|
|
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
|
);
|
|
|
|
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
|
sectionId,
|
|
mode: "edit",
|
|
onSave: () => {
|
|
setEditing(false);
|
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: local } });
|
|
},
|
|
onDiscard: () => {
|
|
setLocal({
|
|
...exercise,
|
|
prompts: [
|
|
{ text: "Hello my name is {avatar}, what is yours?", video_url: "" },
|
|
{ text: "Do you work or do you study?", video_url: "" },
|
|
...exercise.prompts.slice(2)
|
|
]
|
|
});
|
|
},
|
|
onMode: () => { },
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (genResult && generating === "context") {
|
|
setEditing(true);
|
|
setLocal(prev => ({
|
|
...prev,
|
|
first_title: genResult[0].first_topic,
|
|
second_title: genResult[0].second_topic,
|
|
prompts: [
|
|
prev.prompts[0],
|
|
prev.prompts[1],
|
|
...genResult[0].prompts.map((item: any) => ({
|
|
text: item,
|
|
video_url: ""
|
|
}))
|
|
]
|
|
}));
|
|
|
|
dispatch({
|
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
|
payload: {
|
|
sectionId,
|
|
module: currentModule,
|
|
field: "genResult",
|
|
value: undefined
|
|
}
|
|
});
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [genResult, generating]);
|
|
|
|
const addPrompt = () => {
|
|
setLocal(prev => ({
|
|
...prev,
|
|
prompts: [...prev.prompts, { text: "", video_url: "" }]
|
|
}));
|
|
};
|
|
|
|
const removePrompt = (index: number) => {
|
|
if (index < 2) return;
|
|
setLocal(prev => ({
|
|
...prev,
|
|
prompts: prev.prompts.filter((_, i) => i !== index)
|
|
}));
|
|
};
|
|
|
|
const updatePrompt = (index: number, text: string) => {
|
|
if (index < 2) return;
|
|
setLocal(prev => {
|
|
const newPrompts = [...prev.prompts];
|
|
newPrompts[index] = { ...newPrompts[index], text };
|
|
return { ...prev, prompts: newPrompts };
|
|
});
|
|
};
|
|
|
|
const isUnedited = local.prompts.length === 2;
|
|
|
|
|
|
useEffect(() => {
|
|
if (genResult && generating === "media") {
|
|
console.log(genResult[0].prompts);
|
|
setLocal({ ...local, prompts: genResult[0].prompts });
|
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: { ...local, prompts: genResult[0].prompts } } });
|
|
dispatch({
|
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
|
payload: {
|
|
sectionId,
|
|
module: currentModule,
|
|
field: "generating",
|
|
value: undefined
|
|
}
|
|
});
|
|
dispatch({
|
|
type: "UPDATE_SECTION_SINGLE_FIELD",
|
|
payload: {
|
|
sectionId,
|
|
module: currentModule,
|
|
field: "genResult",
|
|
value: undefined
|
|
}
|
|
});
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [genResult, generating]);
|
|
|
|
const handlePrevVideo = () => {
|
|
setCurrentVideoIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
};
|
|
|
|
const handleNextVideo = () => {
|
|
setCurrentVideoIndex((prev) =>
|
|
(prev < local.prompts.length - 1 ? prev + 1 : prev)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className='relative pb-4'>
|
|
<Header
|
|
title={`Speaking 1 Script`}
|
|
description='Generate or write the scripts for the videos.'
|
|
editing={editing}
|
|
handleSave={handleSave}
|
|
modeHandle={modeHandle}
|
|
handleDiscard={handleDiscard}
|
|
mode="edit"
|
|
module="speaking"
|
|
/>
|
|
</div>
|
|
{generating && generating === "context" ? (
|
|
<GenLoader module={currentModule} />
|
|
) : (
|
|
<>
|
|
{editing ? (
|
|
<>
|
|
<Card>
|
|
<CardContent>
|
|
<div className="py-2 mt-2">
|
|
<div className="flex flex-row mb-3 gap-4">
|
|
<BsFileText className="h-5 w-5 text-blue-500 mt-1" />
|
|
<h3 className="font-semibold text-xl">Titles</h3>
|
|
</div>
|
|
<div className="flex gap-6 mt-6">
|
|
<div className="flex-1">
|
|
<h2 className="font-semibold text-lg mb-2">First Title</h2>
|
|
<AutoExpandingTextArea
|
|
value={local.first_title || ''}
|
|
onChange={(text) => setLocal(prev => ({ ...prev, first_title: text }))}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[80px] transition-all"
|
|
placeholder="Enter the first title"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<h2 className="font-semibold text-lg mb-2">Second Title</h2>
|
|
<AutoExpandingTextArea
|
|
value={local.second_title || ''}
|
|
onChange={(text) => setLocal(prev => ({ ...prev, second_title: text }))}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[80px] transition-all"
|
|
placeholder="Enter the second title"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between mb-4 mt-6">
|
|
<h2 className="font-semibold text-xl">Questions</h2>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
{local.prompts.length === 2 ? (
|
|
<div className="py-12 text-center bg-gray-200 rounded-lg border-2 border-dashed border-gray-400">
|
|
<p className="text-gray-600">No questions added yet</p>
|
|
</div>
|
|
) : (
|
|
local.prompts.slice(2).map((prompt, index) => (
|
|
<Card key={index}>
|
|
<CardContent>
|
|
<div className="bg-gray-50 rounded-lg pt-4">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<h3 className="font-medium text-gray-700">Question {index + 1}</h3>
|
|
<button
|
|
type="button"
|
|
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-full transition-colors"
|
|
onClick={() => removePrompt(index + 2)}
|
|
>
|
|
<AiOutlineDelete className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
<AutoExpandingTextArea
|
|
value={prompt.text}
|
|
onChange={(text) => updatePrompt(index + 2, text)}
|
|
className="w-full p-3 border border-gray-200 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[80px] transition-all bg-white"
|
|
placeholder={`Enter question ${index + 1}`}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<button
|
|
type="button"
|
|
onClick={addPrompt}
|
|
className="w-full py-3 px-4 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors flex items-center justify-center gap-2 text-gray-600 font-medium"
|
|
>
|
|
<AiOutlinePlus className="h-5 w-5" />
|
|
Add Question
|
|
</button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
) : isUnedited ? (
|
|
<p className="w-full text-gray-600 px-7 py-8 border-2 bg-white rounded-3xl whitespace-pre-line">
|
|
Generate or edit the questions!
|
|
</p>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{local.prompts.every((p) => p.video_url !== "") && (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col items-start gap-3">
|
|
<div className="flex flex-row mb-3 gap-4 w-full justify-between items-center">
|
|
<div className="flex flex-row gap-4">
|
|
<RiVideoLine className="h-5 w-5 text-amber-500 mt-1" />
|
|
<h3 className="font-semibold text-xl">Videos</h3>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={handlePrevVideo}
|
|
disabled={currentVideoIndex === 0}
|
|
className={`p-2 rounded-full ${currentVideoIndex === 0
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: 'text-gray-600 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<FaChevronLeft className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-sm text-gray-600">
|
|
{currentVideoIndex + 1} / {local.prompts.length}
|
|
</span>
|
|
<button
|
|
onClick={handleNextVideo}
|
|
disabled={currentVideoIndex === local.prompts.length - 1}
|
|
className={`p-2 rounded-full ${currentVideoIndex === local.prompts.length - 1
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: 'text-gray-600 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<FaChevronRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4 w-full items-center">
|
|
<div className="w-full">
|
|
<video
|
|
key={local.prompts[currentVideoIndex].video_url}
|
|
controls
|
|
className="w-full rounded-xl"
|
|
>
|
|
<source src={local.prompts[currentVideoIndex].video_url} />
|
|
</video>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
{generating && generating === "media" &&
|
|
<GenLoader module={currentModule} custom="Generating the videos ... This may take a while ..." />
|
|
}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col items-start">
|
|
<div className="flex flex-row mb-4 gap-4">
|
|
<BsFileText className="h-5 w-5 text-blue-500 mt-1" />
|
|
<h3 className="font-semibold text-xl">Titles</h3>
|
|
</div>
|
|
<div className="w-full flex gap-6 mt-6">
|
|
<div className="flex-1">
|
|
<h4 className="font-medium text-gray-700 mb-2">First Title</h4>
|
|
<div className="w-full px-4 py-3 bg-white shadow-inner rounded-lg border border-gray-100">
|
|
<p className="text-lg">{local.first_title || 'No first title'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1">
|
|
<h4 className="font-medium text-gray-700 mb-2">Second Title</h4>
|
|
<div className="w-full px-4 py-3 bg-white shadow-inner rounded-lg border border-gray-100">
|
|
<p className="text-lg">{local.second_title || 'No second title'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col items-start gap-3">
|
|
<div className="flex flex-row mb-3 gap-4">
|
|
<AiOutlineUnorderedList className="h-5 w-5 text-purple-500 mt-1" />
|
|
<h3 className="font-semibold text-xl">Questions</h3>
|
|
</div>
|
|
<div className="w-full space-y-4">
|
|
{local.prompts.slice(2)
|
|
.filter(prompt => prompt.text !== "")
|
|
.map((prompt, index) => (
|
|
<div key={index} className="bg-white shadow-inner rounded-lg border border-gray-100 p-4">
|
|
<h4 className="font-medium text-gray-700 mb-2">Question {index + 1}</h4>
|
|
<p className="text-gray-700">{prompt.text}</p>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Speaking1; |