feat: banner generation, improved memory system, streaming text animation
- Add banner/cover generation for stories with character reference support - Improve summary system: generate every 8 msgs or when context large - Enhance summary prompt to preserve critical story info (promises, relationships) - Add typewriter text animation during AI streaming - Increase context to 20 messages, lower summary temperature to 0.1 - Server: auto-truncate long messages instead of rejecting
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
getNPCCharacters,
|
||||
createNPCCharacter,
|
||||
} from "../services/api";
|
||||
import { generateAvatarUrl } from "../services/imageGen";
|
||||
import { generateAvatarUrl, generateBannerUrl } from "../services/imageGen";
|
||||
import { useStoryGeneration } from "../hooks/useStoryGeneration";
|
||||
import type {
|
||||
Character,
|
||||
@@ -92,6 +92,8 @@ export default function CreateStoryPage() {
|
||||
const [generatingAvatarIndex, setGeneratingAvatarIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [generatingBanner, setGeneratingBanner] = useState(false);
|
||||
const [coverImage, setCoverImage] = useState("");
|
||||
const [savedNPCs, setSavedNPCs] = useState<NPCCharacter[]>([]);
|
||||
const [showNPCSelector, setShowNPCSelector] = useState(false);
|
||||
|
||||
@@ -154,6 +156,7 @@ export default function CreateStoryPage() {
|
||||
const story = await getStory(id);
|
||||
|
||||
if (story) {
|
||||
setCoverImage(story.coverImage || "");
|
||||
setForm({
|
||||
title: story.title,
|
||||
description: story.description || "",
|
||||
@@ -382,6 +385,37 @@ export default function CreateStoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Генерация баннера/обложки
|
||||
const handleGenerateBanner = async () => {
|
||||
if (!form.title && !form.description && !form.summary) {
|
||||
alert("Заполните название или описание для генерации баннера");
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneratingBanner(true);
|
||||
try {
|
||||
// Передаём персонажей для поиска референса
|
||||
const charactersWithAvatars = form.characters
|
||||
.filter((c) => c.name && c.avatarUrl)
|
||||
.map((c) => ({ name: c.name, avatarUrl: c.avatarUrl }));
|
||||
|
||||
const url = await generateBannerUrl({
|
||||
title: form.title,
|
||||
description: form.description || form.summary,
|
||||
genre: form.genres,
|
||||
setting: form.settings,
|
||||
isNsfw: form.isNsfw,
|
||||
characters: charactersWithAvatars,
|
||||
});
|
||||
setCoverImage(url);
|
||||
} catch (error) {
|
||||
console.error("Failed to generate banner:", error);
|
||||
alert("Ошибка при генерации баннера");
|
||||
} finally {
|
||||
setGeneratingBanner(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Правила мира
|
||||
const handleRuleChange = (index: number, value: string) => {
|
||||
const newRules = [...form.worldRules];
|
||||
@@ -430,7 +464,7 @@ export default function CreateStoryPage() {
|
||||
const storyData = {
|
||||
title: form.title,
|
||||
description: form.description || `Исекай история: ${form.title}`,
|
||||
coverImage: "",
|
||||
coverImage: coverImage,
|
||||
language:
|
||||
LANGUAGES.find((l) => l.code === form.language)?.name.split(" ")[1] ||
|
||||
"Русский",
|
||||
@@ -622,9 +656,10 @@ export default function CreateStoryPage() {
|
||||
</p>
|
||||
<div className="temperature-selector">
|
||||
{[
|
||||
{ value: 0.75, label: "🎯 Точный", desc: "0.75" },
|
||||
{ value: 0.9, label: "⚖️ Сбалансированный", desc: "0.9" },
|
||||
{ value: 0.95, label: "✨ Живой", desc: "0.95" },
|
||||
{ value: 1.05, label: "🎨 Креативный", desc: "1.05" },
|
||||
{ value: 1.0, label: "🎨 Креативный", desc: "1.0" },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
@@ -803,6 +838,64 @@ export default function CreateStoryPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Баннер/Обложка */}
|
||||
<section className="form-section">
|
||||
<h2>🖼️ Обложка</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="banner-generator">
|
||||
<div className="banner-preview">
|
||||
{coverImage ? (
|
||||
<img
|
||||
src={coverImage}
|
||||
alt="Обложка истории"
|
||||
className="banner-image"
|
||||
/>
|
||||
) : (
|
||||
<div className="banner-placeholder">
|
||||
<span>📷 Нет обложки</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="banner-controls">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateBanner}
|
||||
className="btn-generate-banner"
|
||||
disabled={generatingBanner}
|
||||
>
|
||||
{generatingBanner
|
||||
? "⏳ Генерация..."
|
||||
: "✨ Сгенерировать обложку"}
|
||||
</button>
|
||||
<p className="field-hint">
|
||||
ИИ создаст обложку на основе названия, описания и жанров
|
||||
истории.
|
||||
</p>
|
||||
<div className="banner-url-input">
|
||||
<input
|
||||
type="url"
|
||||
value={coverImage}
|
||||
onChange={(e) => setCoverImage(e.target.value)}
|
||||
placeholder="Или вставьте URL изображения..."
|
||||
className="url-input"
|
||||
/>
|
||||
{coverImage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCoverImage("")}
|
||||
className="clear-url-btn"
|
||||
title="Удалить обложку"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Первое сообщение */}
|
||||
<section className="form-section">
|
||||
<h2>
|
||||
|
||||
Reference in New Issue
Block a user