660 lines
31 KiB
TypeScript
660 lines
31 KiB
TypeScript
import {fetch} from "@tauri-apps/plugin-http";
|
|
import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react';
|
|
import {
|
|
BarChart2,
|
|
BookMarked,
|
|
BookOpen,
|
|
ChevronRight,
|
|
Clock,
|
|
CloudSun,
|
|
FileText,
|
|
GraduationCap,
|
|
Languages,
|
|
Loader2,
|
|
LucideIcon,
|
|
MessageSquare,
|
|
Music,
|
|
Pencil,
|
|
RotateCw,
|
|
Square,
|
|
User,
|
|
UserPen,
|
|
Wand2,
|
|
X
|
|
} from 'lucide-react';
|
|
import {writingLevel} from "@/lib/constants/user";
|
|
import {
|
|
advancedDialogueTypes,
|
|
advancedNarrativePersons,
|
|
advancedPredefinedType,
|
|
beginnerDialogueTypes,
|
|
beginnerNarrativePersons,
|
|
beginnerPredefinedType,
|
|
intermediateDialogueTypes,
|
|
intermediateNarrativePersons,
|
|
intermediatePredefinedType,
|
|
langues,
|
|
verbalTime
|
|
} from '@/lib/constants/story';
|
|
import {presetStoryType} from '@/lib/utils/story';
|
|
import SelectBox from "@/components/form/SelectBox";
|
|
import TextInput from "@/components/form/TextInput";
|
|
import TextAreaInput from "@/components/form/TextAreaInput";
|
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
|
import {apiPost} from '@/lib/api/client';
|
|
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
|
import {configs} from "@/lib/configs";
|
|
import InputField from "@/components/form/InputField";
|
|
import NumberInput from "@/components/form/NumberInput";
|
|
import Button from "@/components/ui/Button";
|
|
import IconButton from "@/components/ui/IconButton";
|
|
import PulseLoader from "@/components/ui/PulseLoader";
|
|
import {Editor as TipEditor, EditorContent, useEditor} from "@tiptap/react";
|
|
import {convertToHtml} from "@/lib/utils/editor";
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
import Underline from "@tiptap/extension-underline";
|
|
import TextAlign from "@tiptap/extension-text-align";
|
|
import {getSubLevel, isAnthropicEnabled} from "@/lib/utils/quillsense";
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
|
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
|
|
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
|
|
|
|
interface ShortStoryGeneratorProps {
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface TabItem {
|
|
id: number;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
}
|
|
|
|
export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps) {
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {errorMessage, infoMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
|
|
const t = useTranslations();
|
|
const {setTotalPrice, setTotalCredits}: AIUsageContextProps = useContext<AIUsageContextProps>(AIUsageContext)
|
|
|
|
const [tone, setTone] = useState<string>('');
|
|
const [atmosphere, setAtmosphere] = useState<string>('');
|
|
const [verbTense, setVerbTense] = useState<string>('0');
|
|
const [person, setPerson] = useState<string>('0');
|
|
const [characters, setCharacters] = useState<string>('');
|
|
const [language, setLanguage] = useState<string>(session.user?.writingLang?.toString() ?? '0');
|
|
const [dialogueType, setDialogueType] = useState<string>('0');
|
|
const [wordsCount, setWordsCount] = useState<number>(500)
|
|
const [directives, setDirectives] = useState<string>('');
|
|
const [authorLevel, setAuthorLevel] = useState<string>(session.user?.writingLevel?.toString() ?? '0');
|
|
const [presetType, setPresetType] = useState<string>('0');
|
|
|
|
const [activeTab, setActiveTab] = useState<number>(1);
|
|
const [progress, setProgress] = useState<number>(25);
|
|
const [isGenerating, setIsGenerating] = useState<boolean>(false);
|
|
const progressRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [generatedText, setGeneratedText] = useState<string>('');
|
|
const [generatedStoryTitle, setGeneratedStoryTitle] = useState<string>('');
|
|
const [resume, setResume] = useState<string>('');
|
|
const [totalWordsCount, setTotalWordsCount] = useState<number>(0);
|
|
|
|
const [hasGenerated, setHasGenerated] = useState<boolean>(false);
|
|
const [abortController, setAbortController] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
|
|
|
|
const [useExplicit, setUseExplicit] = useState<boolean>(false);
|
|
const [useSmart, setUseSmart] = useState<boolean>(false);
|
|
|
|
useEffect((): void => {
|
|
if (progressRef.current) {
|
|
progressRef.current.style.width = `${progress}%`;
|
|
}
|
|
}, [progress]);
|
|
|
|
const anthropicEnabled: boolean = isAnthropicEnabled(session);
|
|
const isSubTierTwo: boolean = getSubLevel(session) >= 2;
|
|
const hasAccess: boolean = anthropicEnabled || 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 => {
|
|
presetStoryType(
|
|
presetType,
|
|
setTone,
|
|
setAtmosphere,
|
|
setVerbTense,
|
|
setPerson,
|
|
setDialogueType,
|
|
(): void => {
|
|
},
|
|
);
|
|
}, [presetType]);
|
|
|
|
useEffect((): void => {
|
|
setProgress(activeTab * 25);
|
|
}, [activeTab]);
|
|
|
|
useEffect((): void => {
|
|
if (editor)
|
|
editor.commands.setContent(convertToHtml(generatedText))
|
|
getWordCount();
|
|
}, [editor, generatedText]);
|
|
|
|
async function handleStopGeneration(): Promise<void> {
|
|
if (abortController) {
|
|
await abortController.cancel();
|
|
setAbortController(null);
|
|
infoMessage(t("shortStoryGenerator.result.abortSuccess"));
|
|
}
|
|
}
|
|
|
|
async function handleGeneration(): Promise<void> {
|
|
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,
|
|
useExplicit: useExplicit,
|
|
useSmart: useSmart,
|
|
}),
|
|
});
|
|
|
|
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<Uint8Array> | 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<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 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);
|
|
}
|
|
|
|
// Le message final du endpoint avec title, totalPrice, useYourKey, totalCost
|
|
if (data.title && data.useYourKey !== undefined && data.totalPrice !== undefined) {
|
|
setGeneratedStoryTitle(data.title);
|
|
if (data.useYourKey) {
|
|
setTotalPrice((prev: number): number => prev + data.totalPrice!);
|
|
} else {
|
|
setTotalCredits(data.totalPrice);
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
errorMessage(t("shortStoryGenerator.result.parsingError"));
|
|
}
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
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<void> {
|
|
let content: Record<string, unknown> | string = '';
|
|
if (editor) content = editor.state.doc.toJSON();
|
|
try {
|
|
const bookId: string = await apiPost<string>(
|
|
`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 (
|
|
<div
|
|
className="fixed inset-0 flex items-center justify-center p-4 bg-darkest-background/60 z-50 backdrop-blur-md animate-fadeIn">
|
|
<div
|
|
className="relative bg-tertiary text-text-primary rounded-xl overflow-hidden w-full max-w-md p-6">
|
|
<h2 className="flex items-center font-['ADLaM_Display'] text-xl text-text-primary mb-4">
|
|
<Wand2 className="mr-3 w-5 h-5" strokeWidth={1.75}/>
|
|
{t("shortStoryGenerator.accessDenied.title")}
|
|
</h2>
|
|
<p className="text-text-secondary mb-6">
|
|
{t("shortStoryGenerator.accessDenied.message")}
|
|
</p>
|
|
<Button variant="primary" onClick={onClose} fullWidth>
|
|
{t("shortStoryGenerator.accessDenied.close")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 flex items-center justify-center p-4 bg-darkest-background/60 z-40 backdrop-blur-md animate-fadeIn">
|
|
<div
|
|
className="relative bg-tertiary text-text-primary rounded-xl max-h-[90vh] overflow-hidden flex flex-col w-full max-w-4xl">
|
|
|
|
<div className="flex justify-between items-center px-6 py-4">
|
|
<h2 className="font-['ADLaM_Display'] text-xl text-text-primary flex items-center">
|
|
<Wand2 className="mr-3 w-5 h-5" strokeWidth={1.75}/>
|
|
{t("shortStoryGenerator.title")}
|
|
</h2>
|
|
<IconButton icon={X} variant="light" onClick={onClose} disabled={isGenerating}/>
|
|
</div>
|
|
|
|
<div className="flex-1 min-h-0 bg-darkest-background rounded-xl mx-2 flex flex-col overflow-hidden">
|
|
<div className="px-4 pt-4 pb-2">
|
|
<div className="w-full bg-secondary rounded-full h-2">
|
|
<div
|
|
ref={progressRef}
|
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex border-b border-secondary">
|
|
{([
|
|
{id: 1, label: t("shortStoryGenerator.tabs.basics"), icon: BookOpen},
|
|
{id: 2, label: t("shortStoryGenerator.tabs.structure"), icon: UserPen},
|
|
{id: 3, label: t("shortStoryGenerator.tabs.atmosphere"), icon: CloudSun},
|
|
...(hasGenerated || isGenerating ? [{
|
|
id: 4,
|
|
label: t("shortStoryGenerator.tabs.result"),
|
|
icon: FileText
|
|
}] : [])
|
|
] satisfies TabItem[]).map((tab: TabItem): React.JSX.Element => {
|
|
const TabIcon: LucideIcon = tab.icon;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={(): void => setActiveTab(tab.id)}
|
|
disabled={isGenerating}
|
|
className={`flex items-center px-6 py-3 font-medium transition-colors ${
|
|
activeTab === tab.id
|
|
? 'text-primary border-b-2 border-primary bg-primary/5'
|
|
: 'text-text-secondary hover:text-text-primary'
|
|
}`}
|
|
>
|
|
<TabIcon className="mr-2 w-4 h-4" strokeWidth={1.75}/>
|
|
{tab.label}
|
|
{tab.id === 4 && isGenerating && !generatedText && (
|
|
<Loader2 className="ml-2 animate-spin w-4 h-4" strokeWidth={1.75}/>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex-1 min-h-0 overflow-auto custom-scrollbar">
|
|
{activeTab === 1 && (
|
|
<div className="p-5 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InputField
|
|
icon={GraduationCap}
|
|
fieldName={t("shortStoryGenerator.fields.complexity")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setAuthorLevel(e.target.value)}
|
|
data={writingLevel}
|
|
defaultValue={authorLevel}
|
|
/>
|
|
}
|
|
/>
|
|
<InputField
|
|
icon={BookOpen}
|
|
fieldName={t("shortStoryGenerator.fields.preset")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setPresetType(e.target.value)}
|
|
data={
|
|
authorLevel === '1'
|
|
? beginnerPredefinedType
|
|
: authorLevel === '2'
|
|
? intermediatePredefinedType
|
|
: advancedPredefinedType
|
|
}
|
|
defaultValue={presetType}
|
|
/>
|
|
}
|
|
/>
|
|
<InputField
|
|
icon={Languages}
|
|
fieldName={t("shortStoryGenerator.fields.language")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setLanguage(e.target.value)}
|
|
data={langues}
|
|
defaultValue={language}
|
|
/>
|
|
}
|
|
/>
|
|
<InputField
|
|
icon={BarChart2}
|
|
fieldName={t("shortStoryGenerator.fields.wordCount")}
|
|
input={
|
|
<NumberInput
|
|
value={wordsCount}
|
|
setValue={setWordsCount}
|
|
placeholder="500"
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 2 && (
|
|
<div className="p-5 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InputField
|
|
icon={Clock}
|
|
fieldName={t("shortStoryGenerator.fields.tense")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setVerbTense(e.target.value)}
|
|
data={verbalTime}
|
|
defaultValue={verbTense}
|
|
/>
|
|
}
|
|
/>
|
|
<InputField
|
|
icon={UserPen}
|
|
fieldName={t("shortStoryGenerator.fields.narrative")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setPerson(e.target.value)}
|
|
data={
|
|
authorLevel === '1'
|
|
? beginnerNarrativePersons
|
|
: authorLevel === '2'
|
|
? intermediateNarrativePersons
|
|
: advancedNarrativePersons
|
|
}
|
|
defaultValue={person}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<InputField
|
|
icon={MessageSquare}
|
|
fieldName={t("shortStoryGenerator.fields.dialogue")}
|
|
input={
|
|
<SelectBox
|
|
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setDialogueType(e.target.value)}
|
|
data={
|
|
authorLevel === '1'
|
|
? beginnerDialogueTypes
|
|
: authorLevel === '2'
|
|
? intermediateDialogueTypes
|
|
: advancedDialogueTypes
|
|
}
|
|
defaultValue={dialogueType}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
icon={Pencil}
|
|
fieldName={t("shortStoryGenerator.fields.directives")}
|
|
input={
|
|
<TextAreaInput
|
|
value={directives}
|
|
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setDirectives(e.target.value)}
|
|
placeholder={t("shortStoryGenerator.placeholders.directives")}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 3 && (
|
|
<div className="p-5 space-y-6">
|
|
<InputField
|
|
icon={Music}
|
|
fieldName={t("shortStoryGenerator.fields.tone")}
|
|
input={
|
|
<TextInput
|
|
value={tone}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>) => setTone(e.target.value)}
|
|
placeholder={t("shortStoryGenerator.placeholders.tone")}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
icon={CloudSun}
|
|
fieldName={t("shortStoryGenerator.fields.atmosphere")}
|
|
input={
|
|
<TextInput
|
|
value={atmosphere}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>) => setAtmosphere(e.target.value)}
|
|
placeholder={t("shortStoryGenerator.placeholders.atmosphere")}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
icon={User}
|
|
fieldName={t("shortStoryGenerator.fields.character")}
|
|
input={
|
|
<TextInput
|
|
value={characters}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>) => setCharacters(e.target.value)}
|
|
placeholder={t("shortStoryGenerator.placeholders.character")}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<AdvancedGenerationOptions
|
|
useExplicit={useExplicit}
|
|
setUseExplicit={setUseExplicit}
|
|
useSmart={useSmart}
|
|
setUseSmart={setUseSmart}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 4 && (
|
|
<div className="p-5">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-semibold text-lg">
|
|
{generatedStoryTitle || t("shortStoryGenerator.result.title")}
|
|
</h3>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{isGenerating ? (
|
|
<IconButton icon={Square} variant="danger" onClick={handleStopGeneration}
|
|
tooltip={t("shortStoryGenerator.actions.stop")}/>
|
|
) : generatedText && (
|
|
<>
|
|
<IconButton icon={RotateCw} variant="muted" onClick={handleGeneration}
|
|
tooltip={t("shortStoryGenerator.actions.regenerate")}/>
|
|
<IconButton icon={BookMarked} variant="primary" onClick={handleSave}
|
|
tooltip={t("shortStoryGenerator.actions.save")}/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isGenerating && !generatedText ? (
|
|
<PulseLoader text={t("shortStoryGenerator.result.generating")} size="lg"/>
|
|
) : (
|
|
<div
|
|
className="rounded-lg p-6 overflow-auto max-h-96 fade-in-text">
|
|
<EditorContent editor={editor} className="prose prose-invert max-w-none"/>
|
|
</div>
|
|
)}
|
|
|
|
{generatedText && (
|
|
<div className="flex justify-between items-center mt-4 pt-4 border-t border-secondary">
|
|
<div className="flex items-center text-sm text-text-secondary">
|
|
<BarChart2 className="mr-2 w-4 h-4" strokeWidth={1.75}/>
|
|
{totalWordsCount} {t("shortStoryGenerator.result.words")}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center px-6 py-4">
|
|
<Button variant="ghost" onClick={(): void => setActiveTab(Math.max(1, activeTab - 1))}
|
|
disabled={activeTab === 1 || isGenerating}>
|
|
<ChevronRight className="rotate-180 w-4 h-4" strokeWidth={1.75}/>
|
|
{t("shortStoryGenerator.navigation.previous")}
|
|
</Button>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<Button variant="secondary" onClick={onClose} disabled={isGenerating}>
|
|
{activeTab === 4 && hasGenerated ? t("shortStoryGenerator.navigation.close") : t("shortStoryGenerator.navigation.cancel")}
|
|
</Button>
|
|
|
|
{activeTab < 3 ? (
|
|
<Button variant="primary" onClick={(): void => setActiveTab(activeTab + 1)}
|
|
disabled={isGenerating}>
|
|
{t("shortStoryGenerator.navigation.next")}
|
|
<ChevronRight className="w-4 h-4" strokeWidth={1.75}/>
|
|
</Button>
|
|
) : activeTab === 3 && (
|
|
<Button variant="primary" icon={Wand2} onClick={handleGeneration}
|
|
disabled={isGenerating} isLoading={isGenerating}
|
|
loadingText={t("shortStoryGenerator.actions.generating")}>
|
|
{t("shortStoryGenerator.actions.generate")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |