first commit
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
// API сервис для работы с бэкендом
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";
|
||||
|
||||
// ============ AUTH ============
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
discordId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export function getDiscordLoginUrl(): string {
|
||||
return `${API_URL}/auth/discord`;
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/me`, {
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.user;
|
||||
} catch (error) {
|
||||
console.error("Failed to get current user:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/logout`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to logout:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDiscordAvatarUrl(user: User): string {
|
||||
if (user.avatar) {
|
||||
return `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png`;
|
||||
}
|
||||
// Default Discord avatar
|
||||
const defaultAvatarIndex = parseInt(user.discordId) % 5;
|
||||
return `https://cdn.discordapp.com/embed/avatars/${defaultAvatarIndex}.png`;
|
||||
}
|
||||
|
||||
// ============ STORIES ============
|
||||
|
||||
import type { Story } from "../types";
|
||||
|
||||
export async function getStories(): Promise<Story[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stories`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch stories");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to get stories:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStory(id: string): Promise<Story | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stories/${id}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to get story:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createStory(
|
||||
story: Omit<Story, "id" | "createdAt" | "updatedAt">,
|
||||
): Promise<Story | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stories`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(story),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create story");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { ...data, id: data._id };
|
||||
} catch (error) {
|
||||
console.error("Failed to create story:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateStory(
|
||||
id: string,
|
||||
story: Partial<Story>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stories/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(story),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to update story:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteStory(id: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stories/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete story:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ GAME SESSIONS ============
|
||||
|
||||
import type { GameSession } from "../types";
|
||||
|
||||
export async function getSession(storyId: string): Promise<GameSession | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/sessions/${storyId}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to get session:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSession(
|
||||
storyId: string,
|
||||
session: Omit<GameSession, "storyId">,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/sessions/${storyId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(session),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to save session:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ PLAYER CHARACTERS ============
|
||||
|
||||
import type { PlayerCharacter } from "../types";
|
||||
|
||||
export async function getPlayerCharacters(): Promise<PlayerCharacter[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/characters`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch characters");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.map((c: any) => ({ ...c, id: c._id || c.id }));
|
||||
} catch (error) {
|
||||
console.error("Failed to get characters:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlayerCharacter(
|
||||
id: string,
|
||||
): Promise<PlayerCharacter | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/characters/${id}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { ...data, id: data._id || data.id };
|
||||
} catch (error) {
|
||||
console.error("Failed to get character:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPlayerCharacter(
|
||||
character: Omit<PlayerCharacter, "id" | "userId" | "createdAt" | "updatedAt">,
|
||||
): Promise<PlayerCharacter | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/characters`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(character),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create character");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { ...data, id: data._id };
|
||||
} catch (error) {
|
||||
console.error("Failed to create character:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePlayerCharacter(
|
||||
id: string,
|
||||
character: Partial<PlayerCharacter>,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/characters/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(character),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to update character:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePlayerCharacter(id: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/characters/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete character:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
// DeepSeek API сервис для генерации историй
|
||||
|
||||
import type {
|
||||
Story,
|
||||
ChatMessage,
|
||||
PlayerCharacter,
|
||||
GameSession,
|
||||
} from "../types";
|
||||
|
||||
const DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions";
|
||||
|
||||
// Настройки контекста
|
||||
const RECENT_MESSAGES_COUNT = 10; // Последние N сообщений для контекста
|
||||
const SUMMARY_THRESHOLD = 20; // После скольких сообщений генерировать сводку
|
||||
|
||||
// API ключ должен храниться в переменных окружения
|
||||
const getApiKey = () => import.meta.env.VITE_DEEPSEEK_API_KEY || "";
|
||||
|
||||
interface DeepSeekMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface DeepSeekResponse {
|
||||
choices: {
|
||||
message: {
|
||||
content: string;
|
||||
};
|
||||
}[];
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
prompt_cache_hit_tokens?: number;
|
||||
prompt_cache_miss_tokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendMessage(
|
||||
messages: DeepSeekMessage[],
|
||||
temperature: number = 0.8,
|
||||
): Promise<string> {
|
||||
const apiKey = getApiKey();
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"DeepSeek API ключ не настроен. Добавьте VITE_DEEPSEEK_API_KEY в .env файл",
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(DEEPSEEK_API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "deepseek-chat",
|
||||
messages,
|
||||
temperature,
|
||||
max_tokens: 1000,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepSeek API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: DeepSeekResponse = await response.json();
|
||||
|
||||
// Логируем использование кэша (для отладки)
|
||||
if (data.usage) {
|
||||
console.log(
|
||||
`[DeepSeek] Tokens - Prompt: ${data.usage.prompt_tokens}, Completion: ${data.usage.completion_tokens}`,
|
||||
);
|
||||
if (data.usage.prompt_cache_hit_tokens !== undefined) {
|
||||
console.log(
|
||||
`[DeepSeek] Cache - Hit: ${data.usage.prompt_cache_hit_tokens}, Miss: ${data.usage.prompt_cache_miss_tokens}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return data.choices[0]?.message?.content || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит базовый системный промпт (правила стиля) - КЭШИРУЕТСЯ
|
||||
*/
|
||||
export function buildStyleRules(
|
||||
story: Story,
|
||||
player?: PlayerCharacter,
|
||||
): string {
|
||||
const settingInfo = Array.isArray(story.setting)
|
||||
? story.setting.join(", ")
|
||||
: story.setting;
|
||||
|
||||
const playerDescription = player?.description
|
||||
? player.description.replace(/\{user\}/gi, player.name)
|
||||
: "Не указано";
|
||||
|
||||
// NSFW блок
|
||||
const nsfwBlock = story.isNsfw
|
||||
? `
|
||||
=== NSFW РЕЖИМ АКТИВЕН ===
|
||||
Это история для взрослых. Сняты ограничения на:
|
||||
— Сцены физического насилия, крови, травм и смерти
|
||||
— Сцены сексуального характера и интимной близости
|
||||
— Тёмные темы: пытки, жестокость, моральная серость
|
||||
— Откровенные описания тел, ран, боли
|
||||
|
||||
ПРАВИЛА NSFW:
|
||||
1. Описывай сцены детально, но художественно
|
||||
2. Насилие должно иметь последствия и вес
|
||||
3. Интимные сцены — по инициативе игрока
|
||||
4. Не цензурируй происходящее
|
||||
5. Сохраняй атмосферу и тон истории
|
||||
`
|
||||
: "";
|
||||
|
||||
// Если есть пользовательские правила - используем их
|
||||
if (story.narrativeRules && story.narrativeRules.trim()) {
|
||||
return `${story.narrativeRules}
|
||||
${nsfwBlock}
|
||||
=== МЕТАДАННЫЕ ===
|
||||
ЯЗЫК: ${story.language}
|
||||
ЖАНР: ${story.genre.join(", ")}
|
||||
СЕТТИНГ: ${settingInfo}
|
||||
|
||||
=== ПЕРСОНАЖ ИГРОКА ===
|
||||
Имя: ${player?.name || "Герой"}
|
||||
Описание: ${playerDescription}`;
|
||||
}
|
||||
|
||||
// Дефолтные правила для историй без кастомных настроек
|
||||
return `Ты — РассказчикGPT, ведущий интерактивную историю.
|
||||
|
||||
=== МЕТАДАННЫЕ ===
|
||||
ЯЗЫК: ${story.language}
|
||||
ЖАНР: ${story.genre.join(", ")}
|
||||
СЕТТИНГ: ${settingInfo}
|
||||
${nsfwBlock}
|
||||
=== ПРАВИЛА ПОВЕСТВОВАНИЯ ===
|
||||
1. Игрок сам пишет свои действия и реплики
|
||||
2. Вплетай действия игрока в сцену, описывай реакции персонажей и последствия
|
||||
3. НИКОГДА не принимай решений за игрока
|
||||
4. НИКОГДА не задавай вопросы игроку
|
||||
5. НИКОГДА не предлагай варианты действий
|
||||
6. НИКОГДА не додумывай намерения игрока
|
||||
7. Не делай таймскипов без явного указания
|
||||
8. Если действие не написано игроком — оно НЕ произошло
|
||||
|
||||
=== ПРАВИЛА ГЛАВНОГО ГЕРОЯ ===
|
||||
Ты НЕ ИМЕЕШЬ ПРАВА:
|
||||
— описывать физические действия ГГ
|
||||
— описывать его внутренние ощущения, эмоции или мысли
|
||||
— вкладывать в него реплики или слова
|
||||
Пока игрок сам не напишет действие или реплику,
|
||||
герой считается неподвижным, молчащим и наблюдающим.
|
||||
|
||||
=== РЕАКЦИИ НА РЕПЛИКИ ГГ ===
|
||||
Персонажи обязаны явно реагировать на слова ГГ:
|
||||
— отвечать на заданные вопросы
|
||||
— менять тон, поведение или атмосферу
|
||||
— показывать паузы, напряжение, замешательство
|
||||
Запрещено игнорировать реплики ГГ.
|
||||
|
||||
=== ФОРМАТ ДИАЛОГОВ (ОБЯЗАТЕЛЬНО!) ===
|
||||
ВСЕ реплики персонажей оформляй через двойные звёздочки: **"текст реплики"**
|
||||
Пример правильного формата:
|
||||
Бекк нахмурилась. **"Не доверяю ему,"** — процедила она сквозь зубы.
|
||||
Лапис мягко улыбнулась. **"Всё будет хорошо."**
|
||||
|
||||
НЕ используй обычные кавычки без звёздочек!
|
||||
Описание действий и окружения — обычным текстом без звёздочек.
|
||||
Отвечай на языке: ${story.language}
|
||||
|
||||
=== ПЕРСОНАЖ ИГРОКА ===
|
||||
Имя: ${player?.name || "Герой"}
|
||||
Описание: ${playerDescription}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит контекст мира (лор) - КЭШИРУЕТСЯ
|
||||
*/
|
||||
export function buildWorldContext(story: Story): string {
|
||||
const charactersInfo =
|
||||
story.characters.length > 0
|
||||
? story.characters
|
||||
.map((c) => `- ${c.name} (${c.role}): ${c.description}`)
|
||||
.join("\n")
|
||||
: "Не указаны";
|
||||
|
||||
return `
|
||||
=== МИР ===
|
||||
Название: ${story.world.name}
|
||||
Описание: ${story.world.description}
|
||||
Правила мира: ${story.world.rules.join("; ")}
|
||||
|
||||
=== ПЕРСОНАЖИ МИРА ===
|
||||
${charactersInfo}
|
||||
|
||||
=== ОСНОВНОЙ СЮЖЕТ ===
|
||||
${story.plot}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит динамический контекст (состояние + сводка)
|
||||
*/
|
||||
export function buildDynamicContext(session: GameSession): string {
|
||||
const state = session.currentState;
|
||||
const summary = session.storySummary || "История только началась.";
|
||||
const keyEvents = session.keyEvents?.length
|
||||
? session.keyEvents.slice(-5).join("\n- ")
|
||||
: "Пока нет значимых событий.";
|
||||
|
||||
return `
|
||||
=== ТЕКУЩЕЕ СОСТОЯНИЕ ===
|
||||
Локация: ${state.location}
|
||||
Здоровье: ${state.health}%
|
||||
Инвентарь: ${state.inventory.length > 0 ? state.inventory.join(", ") : "Пусто"}
|
||||
|
||||
=== СВОДКА ИСТОРИИ ===
|
||||
${summary}
|
||||
|
||||
=== КЛЮЧЕВЫЕ СОБЫТИЯ ===
|
||||
- ${keyEvents}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Полный системный промпт (для обратной совместимости)
|
||||
*/
|
||||
export function buildSystemPrompt(
|
||||
story: Story,
|
||||
player?: PlayerCharacter,
|
||||
): string {
|
||||
return buildStyleRules(story, player) + "\n" + buildWorldContext(story);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует ответ с оптимизированным контекстом
|
||||
*/
|
||||
export async function generateStoryResponse(
|
||||
story: Story,
|
||||
chatHistory: ChatMessage[],
|
||||
userMessage: string,
|
||||
player?: PlayerCharacter,
|
||||
session?: GameSession,
|
||||
): Promise<string> {
|
||||
// 1. Правила стиля (кэшируется DeepSeek)
|
||||
const styleRules = buildStyleRules(story, player);
|
||||
|
||||
// 2. Контекст мира (кэшируется DeepSeek)
|
||||
const worldContext = buildWorldContext(story);
|
||||
|
||||
// 3. Динамический контекст (состояние + сводка)
|
||||
const dynamicContext = session ? buildDynamicContext(session) : "";
|
||||
|
||||
// 4. Последние N сообщений (не вся история!)
|
||||
const recentMessages = chatHistory.slice(-RECENT_MESSAGES_COUNT);
|
||||
|
||||
// Собираем финальный системный промпт
|
||||
const systemPrompt = styleRules + "\n" + worldContext + "\n" + dynamicContext;
|
||||
|
||||
const messages: DeepSeekMessage[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...recentMessages.map((msg) => ({
|
||||
role: msg.role as "user" | "assistant",
|
||||
content: msg.content,
|
||||
})),
|
||||
{ role: "user", content: userMessage },
|
||||
];
|
||||
|
||||
// Используем temperature из настроек истории (по умолчанию 1.3)
|
||||
return sendMessage(messages, story.temperature || 1.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует сводку истории (вызывать периодически)
|
||||
*/
|
||||
export async function generateStorySummary(
|
||||
story: Story,
|
||||
messages: ChatMessage[],
|
||||
previousSummary?: string,
|
||||
): Promise<string> {
|
||||
// Берём сообщения для суммаризации (исключая последние, они свежие)
|
||||
const messagesToSummarize = messages.slice(0, -RECENT_MESSAGES_COUNT);
|
||||
|
||||
if (messagesToSummarize.length < SUMMARY_THRESHOLD) {
|
||||
return previousSummary || "";
|
||||
}
|
||||
|
||||
const conversationText = messagesToSummarize
|
||||
.map((m) => `${m.role === "user" ? "Игрок" : "Рассказчик"}: ${m.content}`)
|
||||
.join("\n\n");
|
||||
|
||||
const prompt = previousSummary
|
||||
? `Обнови сводку истории, добавив новые события.
|
||||
|
||||
ПРЕДЫДУЩАЯ СВОДКА:
|
||||
${previousSummary}
|
||||
|
||||
НОВЫЕ СОБЫТИЯ:
|
||||
${conversationText}
|
||||
|
||||
Напиши обновлённую сводку (3-5 предложений), сохраняя ключевые моменты:`
|
||||
: `Создай краткую сводку событий этой истории (3-5 предложений):
|
||||
|
||||
${conversationText}
|
||||
|
||||
Сводка должна содержать: что произошло, где находится герой, какие важные решения принял:`;
|
||||
|
||||
const summaryMessages: DeepSeekMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `Ты - помощник для создания сводок историй. Пиши кратко и по существу на ${story.language}.`,
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
];
|
||||
|
||||
return sendMessage(summaryMessages, 0.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает ключевые события из ответа AI
|
||||
*/
|
||||
export async function extractKeyEvents(
|
||||
aiResponse: string,
|
||||
existingEvents: string[] = [],
|
||||
): Promise<string[]> {
|
||||
// Простая эвристика: ищем важные действия
|
||||
const importantPatterns = [
|
||||
/(?:ты |вы )(?:получил|нашёл|победил|убил|спас|встретил|узнал|открыл|достиг)/gi,
|
||||
/(?:новый |новая |новое )(?:квест|задание|способность|предмет|союзник)/gi,
|
||||
/(?:умер|погиб|потерял|предал)/gi,
|
||||
];
|
||||
|
||||
const newEvents: string[] = [];
|
||||
|
||||
for (const pattern of importantPatterns) {
|
||||
const matches = aiResponse.match(pattern);
|
||||
if (matches) {
|
||||
// Извлекаем предложение с событием
|
||||
const sentences = aiResponse.split(/[.!?]/);
|
||||
for (const sentence of sentences) {
|
||||
if (
|
||||
pattern.test(sentence) &&
|
||||
sentence.length > 20 &&
|
||||
sentence.length < 150
|
||||
) {
|
||||
newEvents.push(sentence.trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Объединяем с существующими, ограничиваем до 10
|
||||
const allEvents = [...existingEvents, ...newEvents];
|
||||
return allEvents.slice(-10);
|
||||
}
|
||||
|
||||
export async function generateStoryDescription(
|
||||
title: string,
|
||||
genre: string[],
|
||||
setting: string[],
|
||||
worldDescription: string,
|
||||
): Promise<string> {
|
||||
const messages: DeepSeekMessage[] = [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Ты - писатель исекай историй. Создавай краткие, захватывающие описания для историй.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Создай краткое описание (2-3 предложения) для исекай истории:
|
||||
Название: ${title}
|
||||
Жанр: ${genre.join(", ")}
|
||||
Сеттинг: ${setting.join(", ")}
|
||||
Мир: ${worldDescription}
|
||||
|
||||
Описание должно быть интригующим и заставлять хотеть начать приключение.`,
|
||||
},
|
||||
];
|
||||
|
||||
return sendMessage(messages, 0.7);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Сервис для хранения данных в localStorage
|
||||
|
||||
import type { Story, GameSession } from "../types";
|
||||
|
||||
const STORIES_KEY = "resekai_stories";
|
||||
const SESSIONS_KEY = "resekai_sessions";
|
||||
|
||||
// Истории
|
||||
export function getStories(): Story[] {
|
||||
const data = localStorage.getItem(STORIES_KEY);
|
||||
if (!data) return [];
|
||||
|
||||
const stories = JSON.parse(data);
|
||||
return stories.map((s: Story) => ({
|
||||
...s,
|
||||
createdAt: new Date(s.createdAt),
|
||||
updatedAt: new Date(s.updatedAt),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getStoryById(id: string): Story | undefined {
|
||||
const stories = getStories();
|
||||
return stories.find((s) => s.id === id);
|
||||
}
|
||||
|
||||
export function saveStory(story: Story): void {
|
||||
const stories = getStories();
|
||||
const index = stories.findIndex((s) => s.id === story.id);
|
||||
|
||||
if (index >= 0) {
|
||||
stories[index] = story;
|
||||
} else {
|
||||
stories.push(story);
|
||||
}
|
||||
|
||||
localStorage.setItem(STORIES_KEY, JSON.stringify(stories));
|
||||
}
|
||||
|
||||
export function deleteStory(id: string): void {
|
||||
const stories = getStories().filter((s) => s.id !== id);
|
||||
localStorage.setItem(STORIES_KEY, JSON.stringify(stories));
|
||||
|
||||
// Удаляем связанные сессии
|
||||
const sessions = getSessions().filter((s) => s.storyId !== id);
|
||||
localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
|
||||
}
|
||||
|
||||
// Игровые сессии
|
||||
export function getSessions(): GameSession[] {
|
||||
const data = localStorage.getItem(SESSIONS_KEY);
|
||||
if (!data) return [];
|
||||
|
||||
const sessions = JSON.parse(data);
|
||||
return sessions.map((s: GameSession) => ({
|
||||
...s,
|
||||
createdAt: new Date(s.createdAt),
|
||||
updatedAt: new Date(s.updatedAt),
|
||||
messages: s.messages.map((m) => ({
|
||||
...m,
|
||||
timestamp: new Date(m.timestamp),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getSessionByStoryId(storyId: string): GameSession | undefined {
|
||||
const sessions = getSessions();
|
||||
return sessions.find((s) => s.storyId === storyId);
|
||||
}
|
||||
|
||||
export function getSessionById(id: string): GameSession | undefined {
|
||||
const sessions = getSessions();
|
||||
return sessions.find((s) => s.id === id);
|
||||
}
|
||||
|
||||
export function saveSession(session: GameSession): void {
|
||||
const sessions = getSessions();
|
||||
const index = sessions.findIndex((s) => s.id === session.id);
|
||||
|
||||
if (index >= 0) {
|
||||
sessions[index] = session;
|
||||
} else {
|
||||
sessions.push(session);
|
||||
}
|
||||
|
||||
localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
|
||||
}
|
||||
|
||||
export function deleteSession(id: string): void {
|
||||
const sessions = getSessions().filter((s) => s.id !== id);
|
||||
localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
|
||||
}
|
||||
|
||||
// Генерация ID
|
||||
export function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user