import {ChangeEvent, useContext, useEffect, useState} from "react"; import {Editor, EditorContent, useEditor} from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import TextAlign from "@tiptap/extension-text-align"; import System from "@/lib/models/System"; import {ChapterContext} from "@/context/ChapterContext"; import {BookContext} from "@/context/BookContext"; import {SelectBoxProps} from "@/shared/interface"; import {AlertContext} from "@/context/AlertContext"; import {SessionContext} from "@/context/SessionContext"; import { faCubes, faFeather, faGlobe, faMagicWandSparkles, faMapPin, faPalette, faUser } from "@fortawesome/free-solid-svg-icons"; import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading"; import QSTextGeneratedPreview from "@/components/QSTextGeneratedPreview"; import {EditorContext} from "@/context/EditorContext"; import {useTranslations} from "next-intl"; import QuillSense from "@/lib/models/QuillSense"; import TextInput from "@/components/form/TextInput"; import InputField from "@/components/form/InputField"; import TexteAreaInput from "@/components/form/TexteAreaInput"; import SuggestFieldInput from "@/components/form/SuggestFieldInput"; import Collapse from "@/components/Collapse"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {BookTags} from "@/lib/models/Book"; import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext"; import {configs} from "@/lib/configs"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; interface CompanionContent { version: number; content: string; wordsCount: number; } export default function DraftCompanion() { const t = useTranslations(); const {setTotalPrice, setTotalCredits} = useContext(AIUsageContext) const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); const mainEditor: Editor | null = useEditor({ extensions: [ StarterKit, Underline, TextAlign.configure({ types: ['heading', 'paragraph'], }), ], injectCSS: false, editable: false, immediatelyRender: false, }); const {editor} = useContext(EditorContext); const {chapter} = useContext(ChapterContext); const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const {errorMessage, infoMessage} = useContext(AlertContext); const [draftVersion, setDraftVersion] = useState(0); const [draftWordCount, setDraftWordCount] = useState(0); const [refinedText, setRefinedText] = useState(''); const [isRefining, setIsRefining] = useState(false); const [showRefinedText, setShowRefinedText] = useState(false); const [showEnhancer, setShowEnhancer] = useState(false); const [abortController, setAbortController] = useState | null>(null); const [toneAtmosphere, setToneAtmosphere] = useState(''); const [specifications, setSpecifications] = useState(''); const [characters, setCharacters] = useState([]); const [locations, setLocations] = useState([]); const [objects, setObjects] = useState([]); const [worldElements, setWorldElements] = useState([]); const [taguedCharacters, setTaguedCharacters] = useState([]); const [taguedLocations, setTaguedLocations] = useState([]); const [taguedObjects, setTaguedObjects] = useState([]); const [taguedWorldElements, setTaguedWorldElements] = useState([]); const [searchCharacters, setSearchCharacters] = useState(''); const [searchLocations, setSearchLocations] = useState(''); const [searchObjects, setSearchObjects] = useState(''); const [searchWorldElements, setSearchWorldElements] = useState(''); const [showCharacterSuggestions, setShowCharacterSuggestions] = useState(false); const [showLocationSuggestions, setShowLocationSuggestions] = useState(false); const [showObjectSuggestions, setShowObjectSuggestions] = useState(false); const [showWorldElementSuggestions, setShowWorldElementSuggestions] = useState(false); const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session); const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3; const hasAccess: boolean = (isGPTEnabled || isSubTierTree) && !isCurrentlyOffline() && !book?.localBook; useEffect((): void => { getDraftContent().then(); if (showEnhancer) { fetchTags().then(); } }, [mainEditor, chapter, showEnhancer]); async function getDraftContent(): Promise { try { let response: CompanionContent | null; if (isCurrentlyOffline()) { response = await window.electron.invoke('db:chapter:content:companion', { bookid: book?.bookId, chapterid: chapter?.chapterId, version: chapter?.chapterContent.version, }); } else { if (book?.localBook) { response = await window.electron.invoke('db:chapter:content:companion', { bookid: book?.bookId, chapterid: chapter?.chapterId, version: chapter?.chapterContent.version, }); } else { response = await System.authGetQueryToServer(`chapter/content/companion`, session.accessToken, lang, { bookid: book?.bookId, chapterid: chapter?.chapterId, version: chapter?.chapterContent.version, }); } } if (response && mainEditor) { mainEditor.commands.setContent(JSON.parse(response.content)); setDraftVersion(response.version); setDraftWordCount(response.wordsCount); } else if (response && response.content.length === 0 && mainEditor) { mainEditor.commands.setContent({ "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 1 }, "content": [ { "type": "text", "text": t("draftCompanion.noPreviousVersion") } ] } ] }); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("draftCompanion.unknownError")); } } } async function fetchTags(): Promise { try { let responseTags: BookTags | null; if (isCurrentlyOffline()) { responseTags = await window.electron.invoke('db:book:tags', book?.bookId); } else { if (book?.localBook) { responseTags = await window.electron.invoke('db:book:tags', book?.bookId); } else { responseTags = await System.authGetQueryToServer(`book/tags`, session.accessToken, lang, { bookId: book?.bookId }); } } if (responseTags) { setCharacters(responseTags.characters); setLocations(responseTags.locations); setObjects(responseTags.objects); setWorldElements(responseTags.worldElements); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("draftCompanion.unknownError")); } } } async function handleStopRefining(): Promise { if (abortController) { await abortController.cancel(); setAbortController(null); infoMessage(t("draftCompanion.abortSuccess")); } } async function handleQuillSenseRefined(): Promise { if (chapter && session?.accessToken) { setIsRefining(true); setShowRefinedText(false); setRefinedText(''); try { const response: Response = await fetch(`${configs.apiUrl}quillsense/refine`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.accessToken}`, }, body: JSON.stringify({ chapterId: chapter?.chapterId, bookId: book?.bookId, toneAndAtmosphere: toneAtmosphere, advancedPrompt: specifications, tags: { characters: taguedCharacters, locations: taguedLocations, objects: taguedObjects, worldElements: taguedWorldElements, } }), }); if (!response.ok) { const error: { message?: string } = await response.json(); errorMessage(error.message || t('draftCompanion.errorRefineDraft')); setIsRefining(false); return; } const reader: ReadableStreamDefaultReader | undefined = response.body?.getReader(); const decoder: TextDecoder = new TextDecoder(); let accumulatedText: string = ''; if (!reader) { errorMessage(t('draftCompanion.errorRefineDraft')); setIsRefining(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 dataStr: string = line.slice(6); const data: { content?: string; totalPrice?: number; useYourKey?: boolean; } = JSON.parse(dataStr); if ('content' in data && data.content) { accumulatedText += data.content; setRefinedText(accumulatedText); } else if ('useYourKey' in data && 'totalPrice' in 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) { break; } } setIsRefining(false); setShowRefinedText(true); setAbortController(null); } catch (e: unknown) { setIsRefining(false); setAbortController(null); if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('draftCompanion.unknownErrorRefineDraft')); } } } } function insertText(): void { if (editor && refinedText) { editor.commands.focus('end'); if (editor.getText().length > 0) { editor.commands.insertContent('\n\n'); } editor.commands.insertContent(System.textContentToHtml(refinedText)); setShowRefinedText(false); } } function filteredCharacters(): SelectBoxProps[] { if (searchCharacters.trim().length === 0) return []; return characters .filter((item: SelectBoxProps): boolean => item.label.toLowerCase().includes(searchCharacters.toLowerCase()) && !taguedCharacters.includes(item.value)) .slice(0, 3); } function filteredLocations(): SelectBoxProps[] { if (searchLocations.trim().length === 0) return []; return locations .filter((item: SelectBoxProps): boolean => item.label.toLowerCase().includes(searchLocations.toLowerCase()) && !taguedLocations.includes(item.value)) .slice(0, 3); } function filteredObjects(): SelectBoxProps[] { if (searchObjects.trim().length === 0) return []; return objects .filter((item: SelectBoxProps): boolean => item.label.toLowerCase().includes(searchObjects.toLowerCase()) && !taguedObjects.includes(item.value)) .slice(0, 3); } function filteredWorldElements(): SelectBoxProps[] { if (searchWorldElements.trim().length === 0) return []; return worldElements .filter((item: SelectBoxProps): boolean => item.label.toLowerCase().includes(searchWorldElements.toLowerCase()) && !taguedWorldElements.includes(item.value)) .slice(0, 3); } function handleAddCharacter(value: string): void { if (!taguedCharacters.includes(value)) { const newCharacters: string[] = [...taguedCharacters, value]; setTaguedCharacters(newCharacters); } setSearchCharacters(''); setShowCharacterSuggestions(false); } function handleAddLocation(value: string): void { if (!taguedLocations.includes(value)) { const newLocations: string[] = [...taguedLocations, value]; setTaguedLocations(newLocations); } setSearchLocations(''); setShowLocationSuggestions(false); } function handleAddObject(value: string): void { if (!taguedObjects.includes(value)) { const newObjects: string[] = [...taguedObjects, value]; setTaguedObjects(newObjects); } setSearchObjects(''); setShowObjectSuggestions(false); } function handleAddWorldElement(value: string): void { if (!taguedWorldElements.includes(value)) { const newWorldElements: string[] = [...taguedWorldElements, value]; setTaguedWorldElements(newWorldElements); } setSearchWorldElements(''); setShowWorldElementSuggestions(false); } function handleRemoveCharacter(value: string): void { setTaguedCharacters(taguedCharacters.filter((tag: string): boolean => tag !== value)); } function handleRemoveLocation(value: string): void { setTaguedLocations(taguedLocations.filter((tag: string): boolean => tag !== value)); } function handleRemoveObject(value: string): void { setTaguedObjects(taguedObjects.filter((tag: string): boolean => tag !== value)); } function handleRemoveWorldElement(value: string): void { setTaguedWorldElements(taguedWorldElements.filter((tag: string): boolean => tag !== value)); } function handleCharacterSearch(text: string): void { setSearchCharacters(text); setShowCharacterSuggestions(text.trim().length > 0); } function handleLocationSearch(text: string): void { setSearchLocations(text); setShowLocationSuggestions(text.trim().length > 0); } function handleObjectSearch(text: string): void { setSearchObjects(text); setShowObjectSuggestions(text.trim().length > 0); } function handleWorldElementSearch(text: string): void { setSearchWorldElements(text); setShowWorldElementSuggestions(text.trim().length > 0); } function getCharacterLabel(value: string): string { const character: SelectBoxProps | undefined = characters.find((item: SelectBoxProps): boolean => item.value === value); return character ? character.label : value; } function getLocationLabel(value: string): string { const location: SelectBoxProps | undefined = locations.find((item: SelectBoxProps): boolean => item.value === value); return location ? location.label : value; } function getObjectLabel(value: string): string { const object: SelectBoxProps | undefined = objects.find((item: SelectBoxProps): boolean => item.value === value); return object ? object.label : value; } function getWorldElementLabel(value: string): string { const element: SelectBoxProps | undefined = worldElements.find((item: SelectBoxProps): boolean => item.value === value); return element ? element.label : value; } if (showEnhancer && hasAccess) { return (

Amélioration de texte

) => setToneAtmosphere(e.target.value)} placeholder={t("ghostWriter.tonePlaceholder")} /> } />
} /> handleCharacterSearch(e.target.value)} handleAddTag={handleAddCharacter} handleRemoveTag={handleRemoveCharacter} filteredTags={filteredCharacters} showTagSuggestions={showCharacterSuggestions} setShowTagSuggestions={setShowCharacterSuggestions} getTagLabel={getCharacterLabel} /> handleLocationSearch(e.target.value)} handleAddTag={handleAddLocation} handleRemoveTag={handleRemoveLocation} filteredTags={filteredLocations} showTagSuggestions={showLocationSuggestions} setShowTagSuggestions={setShowLocationSuggestions} getTagLabel={getLocationLabel} /> handleObjectSearch(e.target.value)} handleAddTag={handleAddObject} handleRemoveTag={handleRemoveObject} filteredTags={filteredObjects} showTagSuggestions={showObjectSuggestions} setShowTagSuggestions={setShowObjectSuggestions} getTagLabel={getObjectLabel} /> handleWorldElementSearch(e.target.value)} handleAddTag={handleAddWorldElement} handleRemoveTag={handleRemoveWorldElement} filteredTags={filteredWorldElements} showTagSuggestions={showWorldElementSuggestions} setShowTagSuggestions={setShowWorldElementSuggestions} getTagLabel={getWorldElementLabel} />
} />
) => setSpecifications(e.target.value)} placeholder="Spécifications particulières pour l'amélioration..." maxLength={600} /> } />
{(showRefinedText || isRefining) && ( setShowRefinedText(false)} onRefresh={handleQuillSenseRefined} value={refinedText} onInsert={insertText} isGenerating={isRefining} onStop={handleStopRefining} /> )} ); } return (
{t("draftCompanion.words")}: {draftWordCount}
{ hasAccess && chapter?.chapterContent.version === 3 && (
setShowEnhancer(true)} isLoading={isRefining} text={t("draftCompanion.refine")} loadingText={t("draftCompanion.refining")} icon={faFeather} />
) }
); }