Add components for Act management and integrate Electron setup

This commit is contained in:
natreex
2025-11-16 11:00:04 -05:00
parent e192b6dcc2
commit 8167948881
97 changed files with 25378 additions and 3 deletions

View File

@@ -0,0 +1,426 @@
import React, {ChangeEvent, useContext, useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faBookOpen,
faFileImport,
faFloppyDisk,
faGear,
faGhost,
faHashtag,
faMagicWandSparkles,
faPalette,
faTags,
faX
} from '@fortawesome/free-solid-svg-icons';
import {SessionContext} from "@/context/SessionContext";
import {AlertContext} from "@/context/AlertContext";
import {EditorContext} from "@/context/EditorContext";
import System from "@/lib/models/System";
import {BookContext} from "@/context/BookContext";
import {ChapterContext} from "@/context/ChapterContext";
import QSTextGeneratedPreview from "@/components/QSTextGeneratedPreview";
import TextInput from "@/components/form/TextInput";
import InputField from "@/components/form/InputField";
import RadioBox from "@/components/form/RadioBox";
import TexteAreaInput from "@/components/form/TexteAreaInput";
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
import NumberInput from "@/components/form/NumberInput";
import PanelHeader from "@/components/PanelHeader";
import GhostWriterTags from "@/components/ghostwriter/GhostWriterTags";
import Chapter, {TiptapNode} from "@/lib/models/Chapter";
import GhostWriterSettings from "@/components/ghostwriter/GhostWriterSettings";
import {useTranslations} from "next-intl";
import QuillSense, {AIGeneratedText} from "@/lib/models/QuillSense";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import {LangContext} from "@/context/LangContext";
import {configs} from "@/lib/configs";
export default function GhostWriter() {
const t = useTranslations();
const {lang} = useContext(LangContext)
const {session} = useContext(SessionContext);
const {errorMessage, successMessage, infoMessage} = useContext(AlertContext);
const {editor} = useContext(EditorContext);
const {book} = useContext(BookContext);
const {chapter} = useContext(ChapterContext);
const {setTotalPrice, setTotalCredits} = useContext<AIUsageContextProps>(AIUsageContext);
const [minWords, setMinWords] = useState<number>(500);
const [maxWords, setMaxWords] = useState<number>(1000);
const [toneAtmosphere, setToneAtmosphere] = useState<string>('');
const [directive, setDirective] = useState<string>('');
const [type, setType] = useState<number>(0);
const [isGenerating, setIsGenerating] = useState<boolean>(false);
const [textGenerated, setTextGenerated] = useState<string>('');
const [isTextGenerated, setIsTextGenerated] = useState<boolean>(false);
const [advanceSettings, setAdvanceSettings] = useState<boolean>(false);
const [advancedPrompt, setAdvancedPrompt] = useState<string>('');
const [showTags, setShowTags] = useState<boolean>(false);
const [taguedCharacters, setTaguedCharacters] = useState<string[]>([]);
const [taguedLocations, setTaguedLocations] = useState<string[]>([]);
const [taguedObjects, setTaguedObjects] = useState<string[]>([]);
const [taguedWorldElements, setTaguedWorldElements] = useState<string[]>([]);
const [abortController, setAbortController] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3;
const hasAccess: boolean = isGPTEnabled || isSubTierTree;
async function showAdvanceSetting(): Promise<void> {
if (advanceSettings) {
await handleSaveAdvancedSettings();
setAdvanceSettings(false);
} else {
setAdvanceSettings(true);
}
}
async function handleSaveAdvancedSettings(): Promise<void> {
try {
if (advancedPrompt.trim() === '') {
errorMessage(t('ghostWriter.promptEmpty'));
return;
}
const response: boolean = await System.authPostToServer<boolean>(`quillsense/ghostwriter/advanced-settings`, {
bookId: book?.bookId,
advancedPrompt: advancedPrompt
}, session.accessToken, lang);
if (!response) {
errorMessage(t('ghostWriter.errorSaveAdvanced'));
return;
}
successMessage(t('ghostWriter.successSaveAdvanced'));
setAdvanceSettings(false);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('ghostWriter.errorSave'));
} else {
errorMessage(t('ghostWriter.errorUnknown'));
}
}
}
async function handleStopGeneration(): Promise<void> {
if (abortController) {
await abortController.cancel();
setAbortController(null);
infoMessage(t("ghostWriter.abortSuccess"));
}
}
async function handleGenerateGhostWriter(): Promise<void> {
setIsGenerating(true);
setIsTextGenerated(false);
setTextGenerated('');
try {
let content: string = '';
if (editor?.getText()) {
try {
content = editor?.getText();
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('ghostWriter.errorRetrieveContent'));
} 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,
},
}),
});
if (!response.ok) {
const error: { message?: string } = await response.json();
errorMessage(error.message || t('ghostWriter.errorGenerate'));
setIsGenerating(false);
return;
}
const reader: ReadableStreamDefaultReader<Uint8Array> | 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<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;
totalCost?: number;
totalPrice?: number;
useYourKey?: boolean;
aborted?: boolean;
} = JSON.parse(dataStr);
// Si c'est le message final avec les totaux
if ('totalCost' in data && 'useYourKey' in data && 'totalPrice' in data) {
console.log(data)
if (data.useYourKey) {
setTotalPrice((prev: number): number => prev + data.totalPrice!);
} else {
setTotalCredits(data.totalPrice!);
}
} else if ('content' in data && data.content && data.content !== 'starting') {
accumulatedText += data.content;
setTextGenerated(accumulatedText);
}
} catch (e: unknown) {
console.error('Error parsing SSE data:', e);
}
}
}
} 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<void> {
try {
const response: TiptapNode = await System.authGetQueryToServer<TiptapNode>(
`chapter/content`,
session.accessToken,
lang,
{
chapterid: chapter?.chapterId,
version: 1
},
)
if (!response) {
errorMessage(t('ghostWriter.noContentFound'));
return;
}
const content: string = System.htmlToText(Chapter.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(System.textContentToHtml(textGenerated));
setIsTextGenerated(false);
}
}
if (!hasAccess) {
return (
<div className="flex items-center justify-center h-full">
<div
className="bg-tertiary/90 backdrop-blur-sm p-10 rounded-2xl shadow-2xl text-center border border-secondary/50 max-w-md">
<h2 className="text-2xl font-['ADLaM_Display'] text-text-primary mb-4">{t("ghostWriter.title")}</h2>
<p className="text-muted mb-6 text-lg leading-relaxed">{t("ghostWriter.subscriptionRequired")}</p>
<button
onClick={(): string => window.location.href = '/pricing'}
className="px-6 py-3 bg-primary text-text-primary rounded-xl hover:bg-primary-dark transition-all duration-200 hover:scale-105 shadow-md hover:shadow-lg font-semibold"
>
{t("ghostWriter.subscribe")}
</button>
</div>
</div>
);
}
return (
<div className="bg-secondary/20 backdrop-blur-sm flex flex-col h-full overflow-hidden">
<PanelHeader
title={t("ghostWriter.title")}
description={t("ghostWriter.description")}
badge="AI"
icon={faGhost}
/>
{
showTags ? (
<GhostWriterTags taguedCharacters={taguedCharacters} setTaguedCharacters={setTaguedCharacters}
taguedLocations={taguedLocations} setTaguedLocations={setTaguedLocations}
taguedObjects={taguedObjects} setTaguedObjects={setTaguedObjects}
taguedWorldElements={taguedWorldElements}
setTaguedWorldElements={setTaguedWorldElements}/>
) : !showTags && !advanceSettings ? (
<div className="p-4 lg:p-5 space-y-5 overflow-y-auto flex-grow custom-scrollbar">
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<h3 className="text-text-primary text-lg font-medium mb-4 flex items-center gap-2">
<FontAwesomeIcon icon={faHashtag} className="text-primary w-5 h-5"/>
{t("ghostWriter.length")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputField
fieldName={t("ghostWriter.minimum")}
input={
<NumberInput
value={minWords}
setValue={setMinWords}
placeholder={t("ghostWriter.words")}
/>
}
/>
<InputField
fieldName={t("ghostWriter.maximum")}
input={
<NumberInput
value={maxWords}
setValue={setMaxWords}
placeholder={t("ghostWriter.words")}
/>
}
/>
</div>
</div>
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<div className="mb-1">
<InputField
icon={faBookOpen}
fieldName={t("ghostWriter.type")}
input={<RadioBox selected={type} setSelected={setType} name={'sectionType'}/>}
/>
</div>
</div>
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<InputField
icon={faPalette}
fieldName={t("ghostWriter.toneAtmosphere")}
input={
<TextInput
value={toneAtmosphere}
setValue={(e: ChangeEvent<HTMLInputElement>) => setToneAtmosphere(e.target.value)}
placeholder={t("ghostWriter.tonePlaceholder")}
/>
}
/>
</div>
<div className="bg-secondary/20 rounded-lg p-5 shadow-inner flex-1">
<InputField
icon={faMagicWandSparkles}
fieldName={t("ghostWriter.directive")}
action={importPrompt}
actionIcon={faFileImport}
actionLabel={t("ghostWriter.importPrompt")}
input={
<TexteAreaInput
value={directive}
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setDirective(e.target.value)}
placeholder={t("ghostWriter.directivePlaceholder")}
/>
}
/>
</div>
</div>
) : advanceSettings && (
<GhostWriterSettings advancedPrompt={advancedPrompt} setAdvancedPrompt={setAdvancedPrompt}/>
)
}
<div className="p-5 border-t border-secondary/50 bg-secondary/30 backdrop-blur-sm shrink-0 shadow-inner">
<div className="flex justify-center gap-6">
<button
onClick={showAdvanceSetting}
className="px-6 py-3 rounded-xl font-medium text-base bg-secondary/50 hover:bg-secondary text-primary transition-all duration-200 flex items-center gap-2 shadow-md hover:shadow-lg hover:scale-105 border border-secondary/50"
>
<FontAwesomeIcon icon={advanceSettings ? faFloppyDisk : faGear} className={'w-5 h-5'}/>
<span>{advanceSettings ? t("ghostWriter.save") : t("ghostWriter.advanced")}</span>
</button>
{
advanceSettings && (
<button
onClick={(): void => setAdvanceSettings(false)}
className="px-6 py-3 rounded-xl font-medium text-base bg-secondary/50 hover:bg-secondary text-error transition-all duration-200 flex items-center gap-2 shadow-md hover:shadow-lg hover:scale-105 border border-secondary/50"
>
<FontAwesomeIcon icon={faX} className={'w-5 h-5'}/>
<span>{t("ghostWriter.cancel")}</span>
</button>
)
}
<button
onClick={(): void => setShowTags(!showTags)}
className="px-6 py-3 rounded-xl font-medium text-base bg-secondary/50 hover:bg-secondary text-primary transition-all duration-200 flex items-center gap-2 shadow-md hover:shadow-lg hover:scale-105 border border-secondary/50"
>
<FontAwesomeIcon icon={faTags} className={'w-5 h-5'}/>
<span>{t("ghostWriter.tags.addTagPlaceholder")}</span>
</button>
<SubmitButtonWLoading
callBackAction={handleGenerateGhostWriter}
isLoading={isGenerating}
text={t("ghostWriter.generate")}
loadingText={t("ghostWriter.generating")}
icon={faMagicWandSparkles}
/>
</div>
</div>
{(isTextGenerated || isGenerating) && (
<QSTextGeneratedPreview
onClose={(): void => setIsTextGenerated(false)}
onRefresh={(): Promise<void> => handleGenerateGhostWriter()}
value={textGenerated}
onInsert={insertText}
isGenerating={isGenerating}
onStop={handleStopGeneration}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,58 @@
import InputField from "@/components/form/InputField";
import TexteAreaInput from "@/components/form/TexteAreaInput";
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useEffect} from "react";
import {faGuilded} from "@fortawesome/free-brands-svg-icons";
import {AlertContext} from "@/context/AlertContext";
import System from "@/lib/models/System";
import {SessionContext} from "@/context/SessionContext";
import {BookContext} from "@/context/BookContext";
import {LangContext, LangContextProps} from "@/context/LangContext";
import {useTranslations} from "next-intl";
interface GhostWriterSettingsProps {
advancedPrompt: string;
setAdvancedPrompt: Dispatch<SetStateAction<string>>;
}
export default function GhostWriterSettings(
{
advancedPrompt,
setAdvancedPrompt
}: GhostWriterSettingsProps) {
const {errorMessage} = useContext(AlertContext);
const {session} = useContext(SessionContext);
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext)
const {book} = useContext(BookContext);
useEffect((): void => {
getAdvancedSettings().catch();
}, []);
async function getAdvancedSettings(): Promise<void> {
try {
const setting: string = await System.authGetQueryToServer<string>(`quillsense/ghostwriter/advanced-settings`, session.accessToken, lang, {
bookId: book?.bookId
});
if (setting) {
setAdvancedPrompt(setting);
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t('ghostwriter.settings.unknownError'));
}
}
}
return (
<div className={`p-4 lg:p-5 space-y-5 overflow-y-auto flex-grow custom-scrollbar`}>
<InputField input={<TexteAreaInput value={advancedPrompt}
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setAdvancedPrompt(e.target.value)}
placeholder={`Information complémentaire pour la génération...`}
maxLength={600}/>}
fieldName={`Prompt additionnel`} icon={faGuilded}/>
</div>
);
}

View File

@@ -0,0 +1,265 @@
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useEffect, useState} from "react";
import {SelectBoxProps} from "@/shared/interface";
import {faCubes, faGlobe, faMapPin, faUser} from "@fortawesome/free-solid-svg-icons";
import System from "@/lib/models/System";
import {SessionContext} from "@/context/SessionContext";
import {AlertContext} from "@/context/AlertContext";
import {BookContext} from "@/context/BookContext";
import {BookTags} from "@/lib/models/Book";
import SuggestFieldInput from "@/components/form/SuggestFieldInput";
import {LangContext, LangContextProps} from "@/context/LangContext";
import {useTranslations} from "next-intl";
interface GhostWriterTagsProps {
taguedCharacters: string[];
setTaguedCharacters: Dispatch<SetStateAction<string[]>>;
taguedLocations: string[];
setTaguedLocations: Dispatch<SetStateAction<string[]>>;
taguedObjects: string[];
setTaguedObjects: Dispatch<SetStateAction<string[]>>;
taguedWorldElements: string[];
setTaguedWorldElements: Dispatch<SetStateAction<string[]>>;
}
export default function GhostWriterTags(
{
taguedCharacters,
setTaguedCharacters,
taguedLocations,
setTaguedLocations,
taguedObjects,
setTaguedObjects,
taguedWorldElements,
setTaguedWorldElements
}: GhostWriterTagsProps) {
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext)
const {book} = useContext(BookContext);
const [characters, setCharacters] = useState<SelectBoxProps[]>([]);
const [locations, setLocations] = useState<SelectBoxProps[]>([]);
const [objects, setObjects] = useState<SelectBoxProps[]>([]);
const [worldElements, setWorldElements] = useState<SelectBoxProps[]>([]);
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);
useEffect((): void => {
fetchData().then();
}, []);
async function fetchData(): Promise<void> {
try {
const tagsResponse: BookTags = await System.authGetQueryToServer<BookTags>(`book/tags`, session.accessToken, lang, {bookId: book?.bookId});
if (tagsResponse) {
setCharacters(tagsResponse.characters);
setLocations(tagsResponse.locations);
setObjects(tagsResponse.objects);
setWorldElements(tagsResponse.worldElements);
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t('ghostwriter.tags.unknownError'));
}
}
}
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;
}
return (
<div className="p-4 lg:p-5 space-y-4 overflow-y-auto flex-grow custom-scrollbar">
<SuggestFieldInput inputFieldName={`Personnages`}
inputFieldIcon={faUser}
searchTags={searchCharacters}
tagued={taguedCharacters}
handleTagSearch={(e: ChangeEvent<HTMLInputElement>): void => 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>
);
}