Fix AI rule drift and add auto-save during streaming
- Add rule reminders after 10+ messages to prevent AI drift - Add auto-save every 5 seconds during streaming - Add beforeunload warning for unsaved changes - Save user message immediately before generating AI response - Use refs for latest session data in async operations - Reduce summary threshold from 20 to 15 messages
This commit is contained in:
+108
-14
@@ -72,6 +72,28 @@ export default function GamePage() {
|
||||
// Refs for throttled streaming updates
|
||||
const streamingBufferRef = useRef("");
|
||||
const lastUpdateRef = useRef(0);
|
||||
// Ref to track latest session for auto-save
|
||||
const sessionRef = useRef<GameSession | null>(null);
|
||||
const pendingMessagesRef = useRef<ChatMessage[]>([]);
|
||||
const lastAutoSaveRef = useRef(0);
|
||||
const hasUnsavedChangesRef = useRef(false);
|
||||
|
||||
// Keep sessionRef in sync
|
||||
useEffect(() => {
|
||||
sessionRef.current = session;
|
||||
}, [session]);
|
||||
|
||||
// Warn before leaving if there are unsaved changes
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasUnsavedChangesRef.current || isLoading) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, [isLoading]);
|
||||
|
||||
// Throttled streaming update (every 50ms instead of every chunk)
|
||||
const updateStreamingContent = useCallback((chunk: string) => {
|
||||
@@ -120,7 +142,7 @@ export default function GamePage() {
|
||||
// Если персонаж не указан в URL, загружаем первого доступного (или избранного)
|
||||
const characters = await getPlayerCharacters();
|
||||
if (characters.length > 0) {
|
||||
character = characters.find(c => c.isFavorite) || characters[0];
|
||||
character = characters.find((c) => c.isFavorite) || characters[0];
|
||||
setPlayerCharacter(character);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +167,12 @@ export default function GamePage() {
|
||||
}
|
||||
// Если сессия пустая (нет сообщений) — запускаем историю
|
||||
if (sessionData.messages.length === 0 && character) {
|
||||
startStory(normalizedStory, sessionData, character, latestSession.id);
|
||||
startStory(
|
||||
normalizedStory,
|
||||
sessionData,
|
||||
character,
|
||||
latestSession.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (characterId) {
|
||||
@@ -293,6 +320,8 @@ export default function GamePage() {
|
||||
const updatedMessages = [...session.messages, userMessage];
|
||||
const tempSession = { ...session, messages: updatedMessages };
|
||||
setSession(tempSession);
|
||||
pendingMessagesRef.current = updatedMessages;
|
||||
hasUnsavedChangesRef.current = true;
|
||||
setInput("");
|
||||
// Reset textarea height
|
||||
if (inputRef.current) {
|
||||
@@ -302,10 +331,39 @@ export default function GamePage() {
|
||||
setError(null);
|
||||
setStreamingContent("");
|
||||
streamingBufferRef.current = "";
|
||||
lastAutoSaveRef.current = Date.now();
|
||||
|
||||
// Immediately save user message
|
||||
try {
|
||||
await apiSaveSession(story.id, currentSessionId, tempSession);
|
||||
} catch (e) {
|
||||
// Continue even if initial save fails
|
||||
}
|
||||
|
||||
// Создаём AbortController для возможности отмены
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
// Auto-save interval during streaming
|
||||
const autoSaveInterval = setInterval(async () => {
|
||||
if (streamingBufferRef.current.trim() && story && currentSessionId) {
|
||||
const partialMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: "assistant",
|
||||
content: streamingBufferRef.current,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const partialSession: GameSession = {
|
||||
...sessionRef.current!,
|
||||
messages: [...pendingMessagesRef.current, partialMessage],
|
||||
};
|
||||
try {
|
||||
await apiSaveSession(story.id, currentSessionId, partialSession);
|
||||
} catch (e) {
|
||||
// Ignore save errors during streaming
|
||||
}
|
||||
}
|
||||
}, 5000); // Auto-save every 5 seconds
|
||||
|
||||
try {
|
||||
// Streaming ответ от AI
|
||||
const response = await generateStoryResponseStream(
|
||||
@@ -318,6 +376,7 @@ export default function GamePage() {
|
||||
abortControllerRef.current.signal,
|
||||
);
|
||||
flushStreamingContent();
|
||||
clearInterval(autoSaveInterval);
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
@@ -334,9 +393,9 @@ export default function GamePage() {
|
||||
session.keyEvents || [],
|
||||
);
|
||||
|
||||
// Генерируем сводку каждые 20 сообщений
|
||||
// Генерируем сводку каждые 15 сообщений
|
||||
let newSummary = session.storySummary;
|
||||
if (allMessages.length % 20 === 0 && allMessages.length > 0) {
|
||||
if (allMessages.length % 15 === 0 && allMessages.length > 0) {
|
||||
newSummary = await generateStorySummary(
|
||||
story,
|
||||
allMessages,
|
||||
@@ -351,20 +410,18 @@ export default function GamePage() {
|
||||
storySummary: newSummary,
|
||||
};
|
||||
|
||||
await apiSaveSession(
|
||||
story.id,
|
||||
currentSessionId,
|
||||
finalSession,
|
||||
);
|
||||
await apiSaveSession(story.id, currentSessionId, finalSession);
|
||||
setSession(finalSession);
|
||||
hasUnsavedChangesRef.current = false;
|
||||
} catch (err) {
|
||||
clearInterval(autoSaveInterval);
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
// Пользователь отменил — сохраняем то что получили
|
||||
if (streamingContent.trim()) {
|
||||
if (streamingBufferRef.current.trim()) {
|
||||
const partialMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: "assistant",
|
||||
content: streamingContent,
|
||||
content: streamingBufferRef.current,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const partialSession: GameSession = {
|
||||
@@ -373,6 +430,7 @@ export default function GamePage() {
|
||||
};
|
||||
await apiSaveSession(story.id, currentSessionId, partialSession);
|
||||
setSession(partialSession);
|
||||
hasUnsavedChangesRef.current = false;
|
||||
}
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : "Произошла ошибка");
|
||||
@@ -383,6 +441,7 @@ export default function GamePage() {
|
||||
setIsLoading(false);
|
||||
setStreamingContent("");
|
||||
abortControllerRef.current = null;
|
||||
pendingMessagesRef.current = [];
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
@@ -470,6 +529,8 @@ export default function GamePage() {
|
||||
|
||||
const tempSession = { ...session, messages: updatedMessages };
|
||||
setSession(tempSession);
|
||||
pendingMessagesRef.current = updatedMessages;
|
||||
hasUnsavedChangesRef.current = true;
|
||||
setEditingMessageId(null);
|
||||
setEditContent("");
|
||||
setIsLoading(true);
|
||||
@@ -479,6 +540,34 @@ export default function GamePage() {
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
// Save immediately
|
||||
try {
|
||||
await apiSaveSession(story.id, currentSessionId, tempSession);
|
||||
} catch (e) {
|
||||
// Continue even if initial save fails
|
||||
}
|
||||
|
||||
// Auto-save interval during streaming
|
||||
const autoSaveInterval = setInterval(async () => {
|
||||
if (streamingBufferRef.current.trim() && story && currentSessionId) {
|
||||
const partialMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: "assistant",
|
||||
content: streamingBufferRef.current,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const partialSession: GameSession = {
|
||||
...sessionRef.current!,
|
||||
messages: [...pendingMessagesRef.current, partialMessage],
|
||||
};
|
||||
try {
|
||||
await apiSaveSession(story.id, currentSessionId, partialSession);
|
||||
} catch (e) {
|
||||
// Ignore save errors during streaming
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
// Генерируем новый ответ ИИ
|
||||
const response = await generateStoryResponseStream(
|
||||
@@ -491,6 +580,7 @@ export default function GamePage() {
|
||||
abortControllerRef.current.signal,
|
||||
);
|
||||
flushStreamingContent();
|
||||
clearInterval(autoSaveInterval);
|
||||
|
||||
// Сохраняем ответ ИИ в текущую версию
|
||||
const finalVersions: MessageVersion[] = [...newVersions];
|
||||
@@ -524,14 +614,16 @@ export default function GamePage() {
|
||||
|
||||
await apiSaveSession(story.id, currentSessionId, finalSession);
|
||||
setSession(finalSession);
|
||||
hasUnsavedChangesRef.current = false;
|
||||
} catch (err) {
|
||||
clearInterval(autoSaveInterval);
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
if (streamingContent.trim()) {
|
||||
if (streamingBufferRef.current.trim()) {
|
||||
// Сохраняем частичный ответ в версию
|
||||
const finalVersions: MessageVersion[] = [...newVersions];
|
||||
finalVersions[newActiveVersion] = {
|
||||
...finalVersions[newActiveVersion],
|
||||
aiResponse: streamingContent,
|
||||
aiResponse: streamingBufferRef.current,
|
||||
};
|
||||
|
||||
const finalUserMessage: ChatMessage = {
|
||||
@@ -542,7 +634,7 @@ export default function GamePage() {
|
||||
const partialMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: "assistant",
|
||||
content: streamingContent,
|
||||
content: streamingBufferRef.current,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const partialSession: GameSession = {
|
||||
@@ -551,6 +643,7 @@ export default function GamePage() {
|
||||
};
|
||||
await apiSaveSession(story.id, currentSessionId, partialSession);
|
||||
setSession(partialSession);
|
||||
hasUnsavedChangesRef.current = false;
|
||||
}
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : "Произошла ошибка");
|
||||
@@ -559,6 +652,7 @@ export default function GamePage() {
|
||||
setIsLoading(false);
|
||||
setStreamingContent("");
|
||||
abortControllerRef.current = null;
|
||||
pendingMessagesRef.current = [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user