import {ChangeEvent, RefObject, useContext, useEffect, useRef, useState} from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import { faBookBookmark, faBookOpen, faChartSimple, faChevronRight, faClock, faCloudSun, faComments, faFileLines, faGraduationCap, faLanguage, faMagicWandSparkles, faMusic, faPencilAlt, faRotateRight, faSpinner, faStop, faUserAstronaut, faUserEdit, faX } from "@fortawesome/free-solid-svg-icons"; import {writingLevel} from "@/lib/models/User"; import Story, { advancedDialogueTypes, advancedNarrativePersons, advancedPredefinedType, beginnerDialogueTypes, beginnerNarrativePersons, beginnerPredefinedType, intermediateDialogueTypes, intermediateNarrativePersons, intermediatePredefinedType, langues, verbalTime } from '@/lib/models/Story'; import SelectBox from "@/components/form/SelectBox"; import TextInput from "@/components/form/TextInput"; import TexteAreaInput from "@/components/form/TexteAreaInput"; import {SessionContext} from "@/context/SessionContext"; import System from "@/lib/models/System"; import {AlertContext} from "@/context/AlertContext"; import {configs} from "@/lib/configs"; import InputField from "@/components/form/InputField"; import NumberInput from "@/components/form/NumberInput"; import {Editor as TipEditor, EditorContent, useEditor} from "@tiptap/react"; import Editor from "@/lib/models/Editor"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import TextAlign from "@tiptap/extension-text-align"; import QuillSense from "@/lib/models/QuillSense"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext"; interface ShortStoryGeneratorProps { onClose: () => void; } export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps) { const {session} = useContext(SessionContext); const {errorMessage, infoMessage} = useContext(AlertContext); const {lang} = useContext(LangContext) const t = useTranslations(); const {setTotalPrice, setTotalCredits} = useContext(AIUsageContext) const [tone, setTone] = useState(''); const [atmosphere, setAtmosphere] = useState(''); const [verbTense, setVerbTense] = useState('0'); const [person, setPerson] = useState('0'); const [characters, setCharacters] = useState(''); const [language, setLanguage] = useState( session.user?.writingLang.toString() ?? '0', ); const [dialogueType, setDialogueType] = useState('0'); const [wordsCount, setWordsCount] = useState(500) const [directives, setDirectives] = useState(''); const [authorLevel, setAuthorLevel] = useState( session.user?.writingLevel.toString() ?? '0', ); const [presetType, setPresetType] = useState('0'); const [activeTab, setActiveTab] = useState(1); const [progress, setProgress] = useState(25); const modalRef: RefObject = useRef(null); const [isGenerating, setIsGenerating] = useState(false); const [generatedText, setGeneratedText] = useState(''); const [generatedStoryTitle, setGeneratedStoryTitle] = useState(''); const [resume, setResume] = useState(''); const [totalWordsCount, setTotalWordsCount] = useState(0); const [hasGenerated, setHasGenerated] = useState(false); const [abortController, setAbortController] = useState | null>(null); const isAnthropicEnabled: boolean = QuillSense.isAnthropicEnabled(session); const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2; const hasAccess: boolean = isAnthropicEnabled || isSubTierTwo; const editor: TipEditor | null = useEditor({ extensions: [ StarterKit, Underline, TextAlign.configure({ types: ['heading', 'paragraph'], }), ], injectCSS: false, immediatelyRender: false, }); useEffect((): () => void => { document.body.style.overflow = 'hidden'; return (): void => { document.body.style.overflow = 'auto'; }; }, []); useEffect((): void => { Story.presetStoryType( presetType, setTone, setAtmosphere, setVerbTense, setPerson, setDialogueType, (): void => { }, ); }, [presetType]); useEffect((): void => { setProgress(activeTab * 25); }, [activeTab]); useEffect((): void => { if (editor) editor.commands.setContent(Editor.convertToHtml(generatedText)) getWordCount(); }, [editor, generatedText]); async function handleStopGeneration(): Promise { if (abortController) { await abortController.cancel(); setAbortController(null); infoMessage(t("shortStoryGenerator.result.abortSuccess")); } } async function handleGeneration(): Promise { setIsGenerating(true); setGeneratedText(''); try { const response: Response = await fetch(`${configs.apiUrl}quillsense/generate/short`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.accessToken}`, }, body: JSON.stringify({ authorLevel: authorLevel, tone: tone, atmosphere: atmosphere, verbTense: verbTense, person: person, characters: characters, language: language, dialogueType: dialogueType, directives: directives, wordsCount: wordsCount }), }); if (!response.ok) { const error: { message?: string } = await response.json(); errorMessage(error.message || t("shortStoryGenerator.result.unknownError")); setIsGenerating(false); return; } setActiveTab(4); setProgress(100); const reader: ReadableStreamDefaultReader | undefined = response.body?.getReader(); const decoder: TextDecoder = new TextDecoder(); let accumulatedText: string = ''; if (!reader) { errorMessage(t("shortStoryGenerator.result.noResponse")); setIsGenerating(false); return; } setAbortController(reader); while (true) { try { const {done, value}: ReadableStreamReadResult = await reader.read(); if (done) break; const chunk: string = decoder.decode(value, {stream: true}); const lines: string[] = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data: { content?: string; title?: string; useYourKey?: boolean; totalPrice?: number; totalCost?: number; } = JSON.parse(line.slice(6)); if (data.content && data.content !== 'starting') { accumulatedText += data.content; setGeneratedText(accumulatedText); } if (data.title) { setGeneratedStoryTitle(data.title); } // Le message final du endpoint avec title, totalPrice, useYourKey, totalCost if (data.useYourKey !== undefined && data.totalPrice !== undefined) { console.log(data); if (data.useYourKey) { setTotalPrice((prev: number): number => prev + data.totalPrice!); } else { setTotalCredits(data.totalPrice); } } } catch (e: unknown) { console.error('Error parsing SSE data:', e); } } } } catch (e: unknown) { // Si le reader est annulé ou une erreur survient, sortir break; } } setIsGenerating(false); setHasGenerated(true); setAbortController(null); } catch (e: unknown) { if (e instanceof Error) { if (e.name !== 'AbortError') { errorMessage(e.message); } } else { errorMessage(t("shortStoryGenerator.result.unknownError")); } setIsGenerating(false); setAbortController(null); } } function getWordCount(): void { if (editor) { try { const content: string = editor?.state.doc.textContent; const texteNormalise: string = content .replace(/'/g, ' ') .replace(/-/g, ' ') .replace(/\s+/g, ' ') .trim(); const mots: string[] = texteNormalise.split(' '); const wordCount: number = mots.filter( (mot: string): boolean => mot.length > 0, ).length; setTotalWordsCount(wordCount); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("shortStoryGenerator.result.unknownError")); } } } } async function handleSave(): Promise { let content: string = ''; if (editor) content = editor?.state?.doc.toJSON(); try { const bookId: string = await System.authPostToServer( `quillsense/generate/add`, { title: generatedStoryTitle, resume: resume, content: content, wordCount: totalWordsCount, tone: tone, atmosphere: atmosphere, verbTense: verbTense, language: language, dialogueType: dialogueType, person: person, authorLevel: authorLevel ? authorLevel : session.user?.writingLevel, }, session.accessToken, lang ); if (!bookId) { errorMessage(t("shortStoryGenerator.result.saveError")); return; } onClose(); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("shortStoryGenerator.result.unknownError")); } } } if (!hasAccess) { return (

{t("shortStoryGenerator.accessDenied.title")}

{t("shortStoryGenerator.accessDenied.message")}

); } return (

{t("shortStoryGenerator.title")}

{[ {id: 1, label: t("shortStoryGenerator.tabs.basics"), icon: faBookOpen}, {id: 2, label: t("shortStoryGenerator.tabs.structure"), icon: faUserEdit}, {id: 3, label: t("shortStoryGenerator.tabs.atmosphere"), icon: faCloudSun}, ...(hasGenerated || isGenerating ? [{ id: 4, label: t("shortStoryGenerator.tabs.result"), icon: faFileLines }] : []) ].map(tab => ( ))}
{activeTab === 1 && (
setAuthorLevel(e.target.value)} data={writingLevel} defaultValue={authorLevel} /> } /> setPresetType(e.target.value)} data={ authorLevel === '1' ? beginnerPredefinedType : authorLevel === '2' ? intermediatePredefinedType : advancedPredefinedType } defaultValue={presetType} /> } /> setLanguage(e.target.value)} data={langues} defaultValue={language} /> } /> } />
)} {activeTab === 2 && (
setVerbTense(e.target.value)} data={verbalTime} defaultValue={verbTense} /> } /> setPerson(e.target.value)} data={ authorLevel === '1' ? beginnerNarrativePersons : authorLevel === '2' ? intermediateNarrativePersons : advancedNarrativePersons } defaultValue={person} /> } />
setDialogueType(e.target.value)} data={ authorLevel === '1' ? beginnerDialogueTypes : authorLevel === '2' ? intermediateDialogueTypes : advancedDialogueTypes } defaultValue={dialogueType} /> } /> ) => setDirectives(e.target.value)} placeholder={t("shortStoryGenerator.placeholders.directives")} /> } />
)} {activeTab === 3 && (
) => setTone(e.target.value)} placeholder={t("shortStoryGenerator.placeholders.tone")} /> } />
) => setAtmosphere(e.target.value)} placeholder={t("shortStoryGenerator.placeholders.atmosphere")} /> } />
) => setCharacters(e.target.value)} placeholder={t("shortStoryGenerator.placeholders.character")} /> } />
)} {activeTab === 4 && (

{generatedStoryTitle || t("shortStoryGenerator.result.title")}

{isGenerating ? ( ) : generatedText && ( <> )}
{isGenerating && !generatedText ? (

{t("shortStoryGenerator.result.generating")}

) : (
)} {generatedText && (
{totalWordsCount} {t("shortStoryGenerator.result.words")}
)}
)}
{activeTab < 3 ? ( ) : activeTab === 3 && ( )}
); }