feat: add Inter font, extract useStoryGeneration hook, fix mobile menu
This commit is contained in:
@@ -8,6 +8,14 @@
|
|||||||
/>
|
/>
|
||||||
<title>ReSekai</title>
|
<title>ReSekai</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts: Inter -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- PWA -->
|
<!-- PWA -->
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="theme-color" content="#667eea" />
|
<meta name="theme-color" content="#667eea" />
|
||||||
|
|||||||
@@ -138,4 +138,179 @@
|
|||||||
.user-name {
|
.user-name {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile menu button (hamburger) */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 5px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn span {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 2px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn.open span:nth-child(1) {
|
||||||
|
transform: rotate(45deg) translate(5px, 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn.open span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn.open span:nth-child(3) {
|
||||||
|
transform: rotate(-45deg) translate(5px, -5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile menu */
|
||||||
|
.mobile-menu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 64px;
|
||||||
|
right: -100%;
|
||||||
|
width: 280px;
|
||||||
|
max-width: 80vw;
|
||||||
|
height: calc(100vh - 64px);
|
||||||
|
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu.open {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav-link {
|
||||||
|
display: block;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav-link:hover,
|
||||||
|
.mobile-nav-link:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-user-name {
|
||||||
|
flex: 1;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logout-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logout-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-login-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
background: #5865f2;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-login-btn:hover {
|
||||||
|
background: #4752c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay */
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 64px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { getDiscordAvatarUrl } from "../services/api";
|
import { getDiscordAvatarUrl } from "../services/api";
|
||||||
@@ -5,11 +6,14 @@ import "./Header.css";
|
|||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { user, isLoading, isAuthenticated, login, logout } = useAuth();
|
const { user, isLoading, isAuthenticated, login, logout } = useAuth();
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const closeMenu = () => setMobileMenuOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<Link to="/" className="logo">
|
<Link to="/" className="logo" onClick={closeMenu}>
|
||||||
<span className="logo-icon">⚔️</span>
|
<span className="logo-icon">⚔️</span>
|
||||||
<span className="logo-text">ReSekai</span>
|
<span className="logo-text">ReSekai</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -57,11 +61,69 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
</svg>
|
</svg>
|
||||||
Войти через Discord
|
Войти
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<button
|
||||||
|
className={`mobile-menu-btn${mobileMenuOpen ? " open" : ""}`}
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
aria-label="Меню"
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu */}
|
||||||
|
<div className={`mobile-menu${mobileMenuOpen ? " open" : ""}`}>
|
||||||
|
<nav className="mobile-nav">
|
||||||
|
<Link to="/" className="mobile-nav-link" onClick={closeMenu}>
|
||||||
|
📚 Истории
|
||||||
|
</Link>
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<Link to="/create" className="mobile-nav-link" onClick={closeMenu}>
|
||||||
|
✨ Создать историю
|
||||||
|
</Link>
|
||||||
|
<Link to="/characters" className="mobile-nav-link" onClick={closeMenu}>
|
||||||
|
👤 Персонажи
|
||||||
|
</Link>
|
||||||
|
<Link to="/npc" className="mobile-nav-link" onClick={closeMenu}>
|
||||||
|
👥 NPC
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
{isAuthenticated && user ? (
|
||||||
|
<div className="mobile-user">
|
||||||
|
<img
|
||||||
|
src={getDiscordAvatarUrl(user)}
|
||||||
|
alt={user.username}
|
||||||
|
className="mobile-user-avatar"
|
||||||
|
/>
|
||||||
|
<span className="mobile-user-name">{user.username}</span>
|
||||||
|
<button onClick={() => { logout(); closeMenu(); }} className="mobile-logout-btn">
|
||||||
|
Выйти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => { login(); closeMenu(); }} className="mobile-login-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
Войти через Discord
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overlay */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="mobile-menu-overlay" onClick={closeMenu} />
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export { useCharacterDetection } from "./useCharacterDetection";
|
|||||||
export { useStreamingResponse } from "./useStreamingResponse";
|
export { useStreamingResponse } from "./useStreamingResponse";
|
||||||
export { useGameSession } from "./useGameSession";
|
export { useGameSession } from "./useGameSession";
|
||||||
export { useLazyMessages } from "./useLazyMessages";
|
export { useLazyMessages } from "./useLazyMessages";
|
||||||
|
export { useStoryGeneration } from "./useStoryGeneration";
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { sendMessage } from "../services/deepseek";
|
||||||
|
import type { Character } from "../types";
|
||||||
|
|
||||||
|
interface StoryGenerationContext {
|
||||||
|
title?: string;
|
||||||
|
summary?: string;
|
||||||
|
genres?: string[];
|
||||||
|
settings?: string[];
|
||||||
|
worldName?: string;
|
||||||
|
worldDescription?: string;
|
||||||
|
isNsfw?: boolean;
|
||||||
|
characters?: Character[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseStoryGenerationResult {
|
||||||
|
isGenerating: boolean;
|
||||||
|
generatePlot: (context: StoryGenerationContext) => Promise<string | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for AI-powered story generation
|
||||||
|
* Uses {user} placeholder for protagonist name
|
||||||
|
*/
|
||||||
|
export function useStoryGeneration(): UseStoryGenerationResult {
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
|
||||||
|
const generatePlot = useCallback(
|
||||||
|
async (context: StoryGenerationContext): Promise<string | null> => {
|
||||||
|
const { title, summary, genres, settings, worldName, worldDescription, isNsfw, characters } = context;
|
||||||
|
|
||||||
|
// Validate minimum requirements
|
||||||
|
if (!title && !summary && (!genres || genres.length === 0) && (!settings || settings.length === 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGenerating(true);
|
||||||
|
try {
|
||||||
|
const contextLines: string[] = [];
|
||||||
|
if (title) contextLines.push(`Название: ${title}`);
|
||||||
|
if (summary) contextLines.push(`Краткое содержание: ${summary}`);
|
||||||
|
if (genres && genres.length > 0) contextLines.push(`Жанры: ${genres.join(", ")}`);
|
||||||
|
if (settings && settings.length > 0) contextLines.push(`Сеттинг: ${settings.join(", ")}`);
|
||||||
|
if (worldName) contextLines.push(`Мир: ${worldName}`);
|
||||||
|
if (worldDescription) contextLines.push(`Описание мира: ${worldDescription}`);
|
||||||
|
if (isNsfw) contextLines.push("NSFW контент разрешён");
|
||||||
|
|
||||||
|
const namedCharacters = characters?.filter(c => c.name.trim()) || [];
|
||||||
|
if (namedCharacters.length > 0) {
|
||||||
|
contextLines.push(`Персонажи: ${namedCharacters.map(c => `${c.name} (${c.role})`).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `На основе следующей информации создай краткий сюжет для интерактивной исекай-истории.
|
||||||
|
|
||||||
|
${contextLines.join("\n")}
|
||||||
|
|
||||||
|
ВАЖНО: Для главного героя всегда используй {user} вместо конкретного имени.
|
||||||
|
|
||||||
|
Напиши сюжет КРАТКО (до 1000 символов) в формате Markdown:
|
||||||
|
# Завязка
|
||||||
|
(Как {user} попадает в этот мир)
|
||||||
|
|
||||||
|
# Основной конфликт
|
||||||
|
(Главная проблема/цель)
|
||||||
|
|
||||||
|
# Ключевые события
|
||||||
|
- Событие 1
|
||||||
|
- Событие 2
|
||||||
|
|
||||||
|
Будь лаконичен! Ответ только на русском языке.`;
|
||||||
|
|
||||||
|
const response = await sendMessage(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "Ты сценарист исекай-историй. Пиши кратко и по делу. Для главного героя ВСЕГДА используй {user}. Максимум 1000 символов.",
|
||||||
|
},
|
||||||
|
{ role: "user", content: prompt },
|
||||||
|
],
|
||||||
|
0.9,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка генерации сюжета:", error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGenerating,
|
||||||
|
generatePlot,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -902,12 +902,51 @@
|
|||||||
.temp-label {
|
.temp-label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Character card mobile */
|
||||||
|
.character-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-avatar-section {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-avatar-preview,
|
||||||
|
.character-avatar-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-fields {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-row select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Character row with two selects */
|
/* Character row with three selects */
|
||||||
.character-row {
|
.character-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,3 +1063,49 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plot generation button */
|
||||||
|
.plot-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-generate-plot {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-generate-plot:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-generate-plot:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.plot-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-generate-plot {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getNPCCharacters,
|
getNPCCharacters,
|
||||||
} from "../services/api";
|
} from "../services/api";
|
||||||
import { generateAvatarUrl } from "../services/imageGen";
|
import { generateAvatarUrl } from "../services/imageGen";
|
||||||
|
import { useStoryGeneration } from "../hooks/useStoryGeneration";
|
||||||
import type {
|
import type {
|
||||||
Character,
|
Character,
|
||||||
CharacterAge,
|
CharacterAge,
|
||||||
@@ -93,6 +94,9 @@ export default function CreateStoryPage() {
|
|||||||
const [savedNPCs, setSavedNPCs] = useState<NPCCharacter[]>([]);
|
const [savedNPCs, setSavedNPCs] = useState<NPCCharacter[]>([]);
|
||||||
const [showNPCSelector, setShowNPCSelector] = useState(false);
|
const [showNPCSelector, setShowNPCSelector] = useState(false);
|
||||||
|
|
||||||
|
// Story generation hook
|
||||||
|
const { isGenerating: generatingPlot, generatePlot } = useStoryGeneration();
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -344,6 +348,31 @@ export default function CreateStoryPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Генерация сюжета от ИИ
|
||||||
|
const handleGeneratePlot = async () => {
|
||||||
|
const result = await generatePlot({
|
||||||
|
title: form.title,
|
||||||
|
summary: form.summary,
|
||||||
|
genres: form.genres,
|
||||||
|
settings: form.settings,
|
||||||
|
worldName: form.worldName,
|
||||||
|
worldDescription: form.worldDescription,
|
||||||
|
isNsfw: form.isNsfw,
|
||||||
|
characters: form.characters,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === null && !form.title && !form.summary && form.genres.length === 0 && form.settings.length === 0) {
|
||||||
|
alert("Заполните хотя бы название, краткое содержание, жанры или сеттинг для генерации сюжета");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setForm(prev => ({ ...prev, plot: result }));
|
||||||
|
} else {
|
||||||
|
alert("Ошибка при генерации сюжета");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Правила мира
|
// Правила мира
|
||||||
const handleRuleChange = (index: number, value: string) => {
|
const handleRuleChange = (index: number, value: string) => {
|
||||||
const newRules = [...form.worldRules];
|
const newRules = [...form.worldRules];
|
||||||
@@ -697,7 +726,17 @@ export default function CreateStoryPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="plot">Полный сюжет *</label>
|
<div className="plot-header">
|
||||||
|
<label htmlFor="plot">Полный сюжет *</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleGeneratePlot}
|
||||||
|
className="btn-generate-plot"
|
||||||
|
disabled={generatingPlot}
|
||||||
|
>
|
||||||
|
{generatingPlot ? "⏳ Генерация..." : "✨ Сгенерировать сюжет"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p className="field-hint">
|
<p className="field-hint">
|
||||||
Основа для ИИ — подробное описание сюжета и ключевых событий.
|
Основа для ИИ — подробное описание сюжета и ключевых событий.
|
||||||
Поддерживается Markdown.
|
Поддерживается Markdown.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface DeepSeekResponse {
|
|||||||
export async function sendMessage(
|
export async function sendMessage(
|
||||||
messages: DeepSeekMessage[],
|
messages: DeepSeekMessage[],
|
||||||
temperature: number = 0.8,
|
temperature: number = 0.8,
|
||||||
|
max_tokens: number = 1000,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const response = await fetch(`${API_BASE}/api/deepseek/chat`, {
|
const response = await fetch(`${API_BASE}/api/deepseek/chat`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -45,7 +46,7 @@ export async function sendMessage(
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages,
|
messages,
|
||||||
temperature,
|
temperature,
|
||||||
max_tokens: 1000,
|
max_tokens,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user