import { useState, useEffect } from "react"; import { useNavigate, Link, useParams } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import { useAuth } from "../contexts/AuthContext"; import { createStory, getStory, updateStory, getNPCCharacters, } from "../services/api"; import { generateAvatarUrl } from "../services/imageGen"; import type { Character, CharacterAge, CharacterGender, NPCCharacter, } from "../types"; import "./CreateStoryPage.css"; const GENRES = [ "Фэнтези", "Магия", "Приключения", "Романтика", "Экшн", "Комедия", "Драма", "Тёмное фэнтези", "LitRPG", "Хорроры", "Sci-Fi", "Повседневность", ]; const SETTINGS = [ "Средневековье", "Восточное фэнтези", "Стимпанк", "Магическая академия", "Королевство демонов", "Мир мечей и магии", "Постапокалипсис", "Параллельный мир", "Современность", "Космос", "Подземелья", "Королевский двор", ]; const LANGUAGES = [ { code: "ru", name: "🇷🇺 Русский" }, { code: "en", name: "🇺🇸 English" }, { code: "ja", name: "🇯🇵 日本語" }, { code: "zh", name: "🇨🇳 中文" }, { code: "ko", name: "🇰🇷 한국어" }, { code: "es", name: "🇪🇸 Español" }, { code: "de", name: "🇩🇪 Deutsch" }, { code: "fr", name: "🇫🇷 Français" }, ]; const CHARACTER_ROLES = [ "Союзник", "Злодей", "Наставник", "Романс", "Нейтральный NPC", "Антагонист", "Комик", ]; const CHARACTER_AGES: { value: CharacterAge; label: string }[] = [ { value: "child", label: "Ребёнок" }, { value: "teenager", label: "Подросток" }, { value: "adult", label: "Взрослый" }, { value: "elderly", label: "Пожилой" }, ]; const CHARACTER_GENDERS: { value: CharacterGender; label: string }[] = [ { value: "female", label: "Женский" }, { value: "male", label: "Мужской" }, ]; export default function CreateStoryPage() { const { id } = useParams<{ id: string }>(); const isEditMode = Boolean(id); const navigate = useNavigate(); const { isAuthenticated } = useAuth(); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(isEditMode); const [generatingAvatarIndex, setGeneratingAvatarIndex] = useState< number | null >(null); const [savedNPCs, setSavedNPCs] = useState([]); const [showNPCSelector, setShowNPCSelector] = useState(false); const [form, setForm] = useState({ title: "", description: "", language: "ru", genres: [] as string[], customGenre: "", settings: [] as string[], customSetting: "", summary: "", plot: "", firstMessage: "", isNsfw: false, temperature: 0.9, // Креативность ИИ narrativeRules: "", // Правила повествования для ИИ // NPC персонажи мира characters: [ { name: "", description: "", role: "Союзник", age: "adult" as CharacterAge, gender: "female" as CharacterGender, }, ] as Character[], // Мир worldName: "", worldDescription: "", worldRules: [""], }); useEffect(() => { if (!isAuthenticated) { navigate("/"); } }, [isAuthenticated, navigate]); // Загрузка сохранённых NPC useEffect(() => { const loadSavedNPCs = async () => { if (isAuthenticated) { const npcs = await getNPCCharacters(); setSavedNPCs(npcs); } }; loadSavedNPCs(); }, [isAuthenticated]); // Загрузка истории для редактирования useEffect(() => { const loadStory = async () => { if (!id || !isAuthenticated) return; setIsLoading(true); const story = await getStory(id); if (story) { setForm({ title: story.title, description: story.description || "", language: story.language || "ru", genres: story.genre || [], customGenre: "", settings: Array.isArray(story.setting) ? story.setting : [story.setting], customSetting: "", summary: story.summary || "", plot: story.plot || "", firstMessage: story.firstMessage || "", isNsfw: story.isNsfw || false, temperature: story.temperature || 0.9, narrativeRules: story.narrativeRules || "", characters: story.characters?.length > 0 ? story.characters : [ { name: "", description: "", role: "Союзник", age: "adult" as CharacterAge, gender: "female" as CharacterGender, }, ], worldName: story.world?.name || "", worldDescription: story.world?.description || "", worldRules: story.world?.rules?.length > 0 ? story.world.rules : [""], }); } else { navigate("/"); } setIsLoading(false); }; loadStory(); }, [id, isAuthenticated, navigate]); // Жанры const handleGenreToggle = (genre: string) => { setForm((prev) => ({ ...prev, genres: prev.genres.includes(genre) ? prev.genres.filter((g) => g !== genre) : [...prev.genres, genre], })); }; const removeGenre = (genre: string) => { setForm((prev) => ({ ...prev, genres: prev.genres.filter((g) => g !== genre), })); }; const addCustomGenre = () => { if ( form.customGenre.trim() && !form.genres.includes(form.customGenre.trim()) ) { setForm((prev) => ({ ...prev, genres: [...prev.genres, prev.customGenre.trim()], customGenre: "", })); } }; const handleCustomGenreKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); addCustomGenre(); } }; // Сеттинги (теги как жанры) const handleSettingToggle = (setting: string) => { setForm((prev) => ({ ...prev, settings: prev.settings.includes(setting) ? prev.settings.filter((s) => s !== setting) : [...prev.settings, setting], })); }; const removeSetting = (setting: string) => { setForm((prev) => ({ ...prev, settings: prev.settings.filter((s) => s !== setting), })); }; const addCustomSetting = () => { if ( form.customSetting.trim() && !form.settings.includes(form.customSetting.trim()) ) { setForm((prev) => ({ ...prev, settings: [...prev.settings, prev.customSetting.trim()], customSetting: "", })); } }; const handleCustomSettingKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); addCustomSetting(); } }; // NPC Персонажи const handleCharacterChange = ( index: number, field: keyof Character, value: string, ) => { const newCharacters = [...form.characters]; newCharacters[index] = { ...newCharacters[index], [field]: value }; setForm((prev) => ({ ...prev, characters: newCharacters })); }; const addCharacter = () => { setForm((prev) => ({ ...prev, characters: [ ...prev.characters, { name: "", description: "", role: "Союзник", age: "adult" as CharacterAge, gender: "female" as CharacterGender, }, ], })); }; const selectSavedNPC = (npc: NPCCharacter) => { setForm((prev) => ({ ...prev, characters: [ ...prev.characters, { name: npc.name, description: npc.description, role: npc.role, age: npc.age, gender: npc.gender || "female", avatarUrl: npc.avatarUrl, }, ], })); setShowNPCSelector(false); }; const removeCharacter = (index: number) => { setForm((prev) => ({ ...prev, characters: prev.characters.filter((_, i) => i !== index), })); }; const handleGenerateAvatar = async (index: number) => { const char = form.characters[index]; if (!char.name && !char.description) { alert("Заполните имя или описание персонажа для генерации аватара"); return; } setGeneratingAvatarIndex(index); try { const avatarUrl = await generateAvatarUrl({ description: char.description || char.role, name: char.name, age: char.age, gender: char.gender, }); const newCharacters = [...form.characters]; newCharacters[index] = { ...newCharacters[index], avatarUrl }; setForm((prev) => ({ ...prev, characters: newCharacters })); } catch (error) { console.error("Ошибка генерации аватара:", error); alert("Ошибка при генерации аватара"); } finally { setGeneratingAvatarIndex(null); } }; // Правила мира const handleRuleChange = (index: number, value: string) => { const newRules = [...form.worldRules]; newRules[index] = value; setForm((prev) => ({ ...prev, worldRules: newRules })); }; const addRule = () => { setForm((prev) => ({ ...prev, worldRules: [...prev.worldRules, ""] })); }; const removeRule = (index: number) => { setForm((prev) => ({ ...prev, worldRules: prev.worldRules.filter((_, i) => i !== index), })); }; // Подсчёт слов const countWords = (text: string) => { return text .trim() .split(/\s+/) .filter((w) => w.length > 0).length; }; const summaryWordCount = countWords(form.summary); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { const allGenres = [...form.genres]; if (form.customGenre.trim()) { allGenres.push(form.customGenre.trim()); } const allSettings = [...form.settings]; if (form.customSetting.trim()) { allSettings.push(form.customSetting.trim()); } const storyData = { title: form.title, description: form.description || `Исекай история: ${form.title}`, coverImage: "", language: LANGUAGES.find((l) => l.code === form.language)?.name.split(" ")[1] || "Русский", genre: allGenres, setting: allSettings, summary: form.summary, plot: form.plot, firstMessage: form.firstMessage, characters: form.characters.filter((c) => c.name.trim()), isNsfw: form.isNsfw, temperature: form.temperature, narrativeRules: form.narrativeRules.trim() || undefined, world: { name: form.worldName, description: form.worldDescription, rules: form.worldRules.filter((r) => r.trim()), }, }; if (isEditMode && id) { const success = await updateStory(id, storyData); if (success) { navigate(`/story/${id}`); } } else { const story = await createStory(storyData); if (story) { navigate(`/story/${story.id}`); } } } catch (error) { console.error("Error saving story:", error); } finally { setIsSubmitting(false); } }; if (isLoading) { return (

Загрузка...

); } return (
← {isEditMode ? "Назад к истории" : "Назад к историям"}

{isEditMode ? "✏️ Редактировать историю" : "✨ Создать новую историю"}

{isEditMode ? "Измени свою исекай историю" : "Настрой свой уникальный исекай мир"}

{/* Основная информация */}

📖 Основная информация

Только для отображения. Не используется ИИ.

setForm((prev) => ({ ...prev, title: e.target.value })) } placeholder="Например: Перерождение в мире магии" required />

ИИ будет отвечать на выбранном языке.

{LANGUAGES.map((lang) => ( ))}

Краткое описание для привлечения внимания. Не используется ИИ.