- Introduce new error messages for syncing and book deletion in `en.json`. - Update `DeleteBook` to support local-only deletion and synced book management. - Refine offline/online behavior with `deleteLocalToo` checkbox and update related state handling. - Extend repository and IPC methods to handle optional IDs for updates. - Add `SyncQueueContext` for queueing offline changes and improving synchronization workflows. - Enhance refined text generation logic in `DraftCompanion` and `GhostWriter` components. - Replace PUT with PATCH for world updates to align with API expectations. - Streamline `AlertBox` by integrating dynamic translation keys for deletion prompts.
600 lines
28 KiB
TypeScript
600 lines
28 KiB
TypeScript
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<AIUsageContextProps>(AIUsageContext)
|
|
const {lang} = useContext<LangContextProps>(LangContext)
|
|
const {isCurrentlyOffline} = useContext<OfflineContextType>(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<number>(0);
|
|
const [draftWordCount, setDraftWordCount] = useState<number>(0);
|
|
const [refinedText, setRefinedText] = useState<string>('');
|
|
const [isRefining, setIsRefining] = useState<boolean>(false);
|
|
const [showRefinedText, setShowRefinedText] = useState<boolean>(false);
|
|
const [showEnhancer, setShowEnhancer] = useState<boolean>(false);
|
|
const [abortController, setAbortController] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
|
|
|
|
const [toneAtmosphere, setToneAtmosphere] = useState<string>('');
|
|
const [specifications, setSpecifications] = useState<string>('');
|
|
|
|
const [characters, setCharacters] = useState<SelectBoxProps[]>([]);
|
|
const [locations, setLocations] = useState<SelectBoxProps[]>([]);
|
|
const [objects, setObjects] = useState<SelectBoxProps[]>([]);
|
|
const [worldElements, setWorldElements] = useState<SelectBoxProps[]>([]);
|
|
|
|
const [taguedCharacters, setTaguedCharacters] = useState<string[]>([]);
|
|
const [taguedLocations, setTaguedLocations] = useState<string[]>([]);
|
|
const [taguedObjects, setTaguedObjects] = useState<string[]>([]);
|
|
const [taguedWorldElements, setTaguedWorldElements] = useState<string[]>([]);
|
|
|
|
const [searchCharacters, setSearchCharacters] = useState<string>('');
|
|
const [searchLocations, setSearchLocations] = useState<string>('');
|
|
const [searchObjects, setSearchObjects] = useState<string>('');
|
|
const [searchWorldElements, setSearchWorldElements] = useState<string>('');
|
|
|
|
const [showCharacterSuggestions, setShowCharacterSuggestions] = useState<boolean>(false);
|
|
const [showLocationSuggestions, setShowLocationSuggestions] = useState<boolean>(false);
|
|
const [showObjectSuggestions, setShowObjectSuggestions] = useState<boolean>(false);
|
|
const [showWorldElementSuggestions, setShowWorldElementSuggestions] = useState<boolean>(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<void> {
|
|
try {
|
|
let response: CompanionContent | null;
|
|
if (isCurrentlyOffline()) {
|
|
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
|
|
bookid: book?.bookId,
|
|
chapterid: chapter?.chapterId,
|
|
version: chapter?.chapterContent.version,
|
|
});
|
|
} else {
|
|
if (book?.localBook) {
|
|
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
|
|
bookid: book?.bookId,
|
|
chapterid: chapter?.chapterId,
|
|
version: chapter?.chapterContent.version,
|
|
});
|
|
} else {
|
|
response = await System.authGetQueryToServer<CompanionContent>(`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<void> {
|
|
try {
|
|
let responseTags: BookTags | null;
|
|
if (isCurrentlyOffline()) {
|
|
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
|
|
} else {
|
|
if (book?.localBook) {
|
|
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
|
|
} else {
|
|
responseTags = await System.authGetQueryToServer<BookTags>(`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<void> {
|
|
if (abortController) {
|
|
await abortController.cancel();
|
|
setAbortController(null);
|
|
infoMessage(t("draftCompanion.abortSuccess"));
|
|
}
|
|
}
|
|
|
|
async function handleQuillSenseRefined(): Promise<void> {
|
|
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<Uint8Array> | 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<Uint8Array> = 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 (
|
|
<div className="flex flex-col h-full min-h-0 overflow-hidden">
|
|
<div
|
|
className="flex items-center justify-between p-4 bg-secondary/30 backdrop-blur-sm border-b border-secondary/50 flex-shrink-0 shadow-sm">
|
|
<h2 className="text-text-primary font-['ADLaM_Display'] text-xl">Amélioration de texte</h2>
|
|
<button
|
|
onClick={(): void => setShowEnhancer(false)}
|
|
className="px-5 py-2.5 bg-secondary/50 hover:bg-secondary text-text-primary rounded-xl transition-all duration-200 hover:scale-105 shadow-md border border-secondary/50 font-medium"
|
|
>
|
|
Retour
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
|
|
<Collapse
|
|
title="Style d'écriture"
|
|
content={
|
|
<div className="space-y-4">
|
|
<InputField
|
|
icon={faPalette}
|
|
fieldName={t("ghostWriter.toneAtmosphere")}
|
|
input={
|
|
<TextInput
|
|
value={toneAtmosphere}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>) => setToneAtmosphere(e.target.value)}
|
|
placeholder={t("ghostWriter.tonePlaceholder")}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<Collapse
|
|
title="Tags contextuels"
|
|
content={
|
|
<div className="space-y-4">
|
|
<SuggestFieldInput inputFieldName={`Personnages`}
|
|
inputFieldIcon={faUser}
|
|
searchTags={searchCharacters}
|
|
tagued={taguedCharacters}
|
|
handleTagSearch={(e) => handleCharacterSearch(e.target.value)}
|
|
handleAddTag={handleAddCharacter}
|
|
handleRemoveTag={handleRemoveCharacter}
|
|
filteredTags={filteredCharacters}
|
|
showTagSuggestions={showCharacterSuggestions}
|
|
setShowTagSuggestions={setShowCharacterSuggestions}
|
|
getTagLabel={getCharacterLabel}
|
|
/>
|
|
|
|
<SuggestFieldInput inputFieldName={`Lieux`}
|
|
inputFieldIcon={faMapPin}
|
|
searchTags={searchLocations}
|
|
tagued={taguedLocations}
|
|
handleTagSearch={(e) => handleLocationSearch(e.target.value)}
|
|
handleAddTag={handleAddLocation}
|
|
handleRemoveTag={handleRemoveLocation}
|
|
filteredTags={filteredLocations}
|
|
showTagSuggestions={showLocationSuggestions}
|
|
setShowTagSuggestions={setShowLocationSuggestions}
|
|
getTagLabel={getLocationLabel}
|
|
/>
|
|
|
|
<SuggestFieldInput inputFieldName={`Objets`}
|
|
inputFieldIcon={faCubes}
|
|
searchTags={searchObjects}
|
|
tagued={taguedObjects}
|
|
handleTagSearch={(e) => handleObjectSearch(e.target.value)}
|
|
handleAddTag={handleAddObject}
|
|
handleRemoveTag={handleRemoveObject}
|
|
filteredTags={filteredObjects}
|
|
showTagSuggestions={showObjectSuggestions}
|
|
setShowTagSuggestions={setShowObjectSuggestions}
|
|
getTagLabel={getObjectLabel}
|
|
/>
|
|
|
|
<SuggestFieldInput inputFieldName={`Éléments mondiaux`}
|
|
inputFieldIcon={faGlobe}
|
|
searchTags={searchWorldElements}
|
|
tagued={taguedWorldElements}
|
|
handleTagSearch={(e) => handleWorldElementSearch(e.target.value)}
|
|
handleAddTag={handleAddWorldElement}
|
|
handleRemoveTag={handleRemoveWorldElement}
|
|
filteredTags={filteredWorldElements}
|
|
showTagSuggestions={showWorldElementSuggestions}
|
|
setShowTagSuggestions={setShowWorldElementSuggestions}
|
|
getTagLabel={getWorldElementLabel}
|
|
/>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
|
|
<InputField
|
|
icon={faMagicWandSparkles}
|
|
fieldName="Spécifications"
|
|
input={
|
|
<TexteAreaInput
|
|
value={specifications}
|
|
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setSpecifications(e.target.value)}
|
|
placeholder="Spécifications particulières pour l'amélioration..."
|
|
maxLength={600}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="p-5 border-t border-secondary/50 bg-secondary/30 backdrop-blur-sm shrink-0 shadow-inner">
|
|
<div className="flex justify-center">
|
|
<SubmitButtonWLoading
|
|
callBackAction={handleQuillSenseRefined}
|
|
isLoading={isRefining}
|
|
text={t("draftCompanion.refine")}
|
|
loadingText={t("draftCompanion.refining")}
|
|
icon={faMagicWandSparkles}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{(showRefinedText || isRefining) && (
|
|
<QSTextGeneratedPreview
|
|
onClose={(): void => setShowRefinedText(false)}
|
|
onRefresh={handleQuillSenseRefined}
|
|
value={refinedText}
|
|
onInsert={insertText}
|
|
isGenerating={isRefining}
|
|
onStop={handleStopRefining}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full min-h-0 overflow-hidden font-['Lora']">
|
|
<div
|
|
className="flex items-center justify-between p-4 bg-secondary/30 backdrop-blur-sm border-b border-secondary/50 flex-shrink-0 font-['ADLaM_Display'] shadow-sm">
|
|
<div className="mr-4 text-primary-light">
|
|
<span>{t("draftCompanion.words")}: </span>
|
|
<span className="text-text-primary">{draftWordCount}</span>
|
|
</div>
|
|
{
|
|
hasAccess && chapter?.chapterContent.version === 3 && (
|
|
<div className="flex gap-2">
|
|
<SubmitButtonWLoading
|
|
callBackAction={(): void => setShowEnhancer(true)}
|
|
isLoading={isRefining}
|
|
text={t("draftCompanion.refine")}
|
|
loadingText={t("draftCompanion.refining")}
|
|
icon={faFeather}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
<div className="flex-1 min-h-0 overflow-auto">
|
|
<EditorContent
|
|
className="w-full h-full tiptap-draft"
|
|
editor={mainEditor}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |