import {fetch} from "@tauri-apps/plugin-http"; import React, {ChangeEvent, useContext, useState} from 'react'; import {BookOpen, FileInput, Hash, Palette, Wand2} from 'lucide-react'; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {EditorContext, EditorContextProps} from "@/context/EditorContext"; import {apiGet} from "@/lib/api/client"; import {htmlToText, textContentToHtml} from "@/lib/utils/html"; import {BookContext, BookContextProps} from "@/context/BookContext"; import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext"; import QSTextGeneratedPreview from "@/components/ui/QSTextGeneratedPreview"; import TextInput from "@/components/form/TextInput"; import InputField from "@/components/form/InputField"; import RadioBox from "@/components/form/RadioBox"; import TextAreaInput from "@/components/form/TextAreaInput"; import Button from "@/components/ui/Button"; import NumberInput from "@/components/form/NumberInput"; import SectionHeader from "@/components/ui/SectionHeader"; import GhostWriterTags from "@/components/ghostwriter/GhostWriterTags"; import {TiptapNode} from "@/lib/types/chapter"; import {convertTiptapToHTML} from "@/lib/utils/tiptap"; import {useTranslations} from '@/lib/i18n'; import {getSubLevel, isAnthropicEnabled} from "@/lib/utils/quillsense"; import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {configs} from "@/lib/configs"; import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions"; export default function GhostWriter() { const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {errorMessage, infoMessage}: AlertContextProps = useContext(AlertContext); const {editor}: EditorContextProps = useContext(EditorContext); const {book}: BookContextProps = useContext(BookContext); const {chapter}: ChapterContextProps = useContext(ChapterContext); const {setTotalPrice, setTotalCredits}: AIUsageContextProps = useContext(AIUsageContext); const [minWords, setMinWords] = useState(500); const [maxWords, setMaxWords] = useState(1000); const [toneAtmosphere, setToneAtmosphere] = useState(''); const [directive, setDirective] = useState(''); const [type, setType] = useState(0); const [isGenerating, setIsGenerating] = useState(false); const [textGenerated, setTextGenerated] = useState(''); const [isTextGenerated, setIsTextGenerated] = useState(false); const [taguedCharacters, setTaguedCharacters] = useState([]); const [taguedLocations, setTaguedLocations] = useState([]); const [taguedObjects, setTaguedObjects] = useState([]); const [taguedWorldElements, setTaguedWorldElements] = useState([]); const [abortController, setAbortController] = useState | null>(null); const [useExplicit, setUseExplicit] = useState(false); const [useSmart, setUseSmart] = useState(false); const isAnthropicKey: boolean = isAnthropicEnabled(session); const isSubTierTwo: boolean = getSubLevel(session) >= 2; const hasAccess: boolean = isAnthropicKey || isSubTierTwo; async function handleStopGeneration(): Promise { if (abortController) { await abortController.cancel(); setAbortController(null); infoMessage(t("ghostWriter.abortSuccess")); } } async function handleGenerateGhostWriter(): Promise { setIsGenerating(true); setIsTextGenerated(false); setTextGenerated(''); try { let content: string = ''; if (editor?.getText()) { try { content = editor?.getText(); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('ghostWriter.errorUnknownRetrieveContent')); } } } const response: Response = await fetch(`${configs.apiUrl}quillsense/ghostwriter/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.accessToken}`, }, body: JSON.stringify({ bookId: book?.bookId, minWords: minWords, maxWords: maxWords, toneAtmosphere: toneAtmosphere, directive: directive, positionType: type, content: content, tags: { characters: taguedCharacters, locations: taguedLocations, objects: taguedObjects, worldElements: taguedWorldElements, }, useExplicit: useExplicit, useSmart: useSmart, }), }); if (!response.ok) { const error: { message?: string } = await response.json(); errorMessage(error.message || t('ghostWriter.errorGenerate')); setIsGenerating(false); return; } const reader: ReadableStreamDefaultReader | undefined = response.body?.getReader(); const decoder: TextDecoder = new TextDecoder(); let accumulatedText: string = ''; if (!reader) { errorMessage(t('ghostWriter.errorGenerate')); 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 dataStr: string = line.slice(6); const data: { content?: string; totalCost?: number; totalPrice?: number; useYourKey?: boolean; } = JSON.parse(dataStr); if ('content' in data && data.content) { accumulatedText += data.content; setTextGenerated(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) { errorMessage(t('ghostWriter.errorProcessingData')); } } } } catch (e: unknown) { break; } } setIsGenerating(false); setIsTextGenerated(true); setAbortController(null); } catch (e: unknown) { setIsGenerating(false); setAbortController(null); if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('ghostWriter.errorUnknown')); } } } async function importPrompt(): Promise { try { const response: TiptapNode = await apiGet( `chapter/content`, session.accessToken, lang, { chapterid: chapter?.chapterId, version: 1 }, ) if (!response) { errorMessage(t('ghostWriter.noContentFound')); return; } const content: string = htmlToText(convertTiptapToHTML(response)); setDirective(content); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('ghostWriter.errorUnknownImport')); } } } function insertText(): void { if (editor && textGenerated) { editor.commands.focus('end'); if (editor.getText().length > 0) { editor.commands.insertContent('\n\n'); } editor.commands.insertContent(textContentToHtml(textGenerated)); setIsTextGenerated(false); } } if (!hasAccess) { return (

{t("ghostWriter.title")}

{t("ghostWriter.subscriptionRequired")}

); } return (
} /> } />
} /> ) => setToneAtmosphere(e.target.value)} placeholder={t("ghostWriter.tonePlaceholder")} /> } /> ) => setDirective(e.target.value)} placeholder={t("ghostWriter.directivePlaceholder")} /> } />
{(isTextGenerated || isGenerating) && ( setIsTextGenerated(false)} onRefresh={(): Promise => handleGenerateGhostWriter()} value={textGenerated} onInsert={insertText} isGenerating={isGenerating} onStop={handleStopGeneration} /> )}
); }