feat: message editing with version history and AI regeneration

This commit is contained in:
Alexej Wolff
2026-02-11 16:34:25 +01:00
parent dae3c88020
commit a0827caabd
3 changed files with 362 additions and 16 deletions
+209 -9
View File
@@ -61,6 +61,8 @@ export default function GamePage() {
const [streamingContent, setStreamingContent] = useState("");
const [showSessionMenu, setShowSessionMenu] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
const [editContent, setEditContent] = useState("");
const abortControllerRef = useRef<AbortController | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
@@ -370,6 +372,145 @@ export default function GamePage() {
}
};
// Начать редактирование сообщения
const handleEditMessage = (messageId: string, content: string) => {
setEditingMessageId(messageId);
setEditContent(content);
};
// Отменить редактирование
const handleCancelEdit = () => {
setEditingMessageId(null);
setEditContent("");
};
// Сохранить редактирование и регенерировать ответ
const handleSaveEdit = async (messageId: string) => {
if (!session || !story || !currentSessionId || !editContent.trim()) return;
const messageIndex = session.messages.findIndex((m) => m.id === messageId);
if (messageIndex === -1) return;
const message = session.messages[messageIndex];
// Инициализируем версии если их нет (первая версия = оригинал)
const versions = message.versions || [{ content: message.content, timestamp: message.timestamp }];
const currentVersionIndex = message.activeVersion || 0;
// Добавляем новую версию
const newVersion = { content: editContent.trim(), timestamp: new Date() };
const newVersions = [...versions, newVersion];
const newActiveVersion = newVersions.length - 1;
// Обновляем сообщение пользователя
const updatedUserMessage: ChatMessage = {
...message,
content: editContent.trim(),
versions: newVersions,
activeVersion: newActiveVersion,
};
// Обрезаем историю до этого сообщения (удаляем последующие)
const messagesUpToEdit = session.messages.slice(0, messageIndex);
const updatedMessages = [...messagesUpToEdit, updatedUserMessage];
const tempSession = { ...session, messages: updatedMessages };
setSession(tempSession);
setEditingMessageId(null);
setEditContent("");
setIsLoading(true);
setError(null);
setStreamingContent("");
abortControllerRef.current = new AbortController();
try {
// Генерируем новый ответ ИИ
const response = await generateStoryResponseStream(
story,
messagesUpToEdit,
editContent.trim(),
(chunk) => {
setStreamingContent((prev) => prev + chunk);
},
playerCharacter || undefined,
session,
abortControllerRef.current.signal,
);
const assistantMessage: ChatMessage = {
id: generateId(),
role: "assistant",
content: response,
timestamp: new Date(),
};
const allMessages = [...updatedMessages, assistantMessage];
const finalSession: GameSession = {
...session,
messages: allMessages,
};
await apiSaveSession(story.id, currentSessionId, finalSession);
setSession(finalSession);
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
if (streamingContent.trim()) {
const partialMessage: ChatMessage = {
id: generateId(),
role: "assistant",
content: streamingContent,
timestamp: new Date(),
};
const partialSession: GameSession = {
...session,
messages: [...updatedMessages, partialMessage],
};
await apiSaveSession(story.id, currentSessionId, partialSession);
setSession(partialSession);
}
} else {
setError(err instanceof Error ? err.message : "Произошла ошибка");
}
} finally {
setIsLoading(false);
setStreamingContent("");
abortControllerRef.current = null;
}
};
// Переключить версию сообщения
const handleSwitchVersion = (messageId: string, direction: "prev" | "next") => {
if (!session) return;
const messageIndex = session.messages.findIndex((m) => m.id === messageId);
if (messageIndex === -1) return;
const message = session.messages[messageIndex];
if (!message.versions || message.versions.length <= 1) return;
const currentVersion = message.activeVersion || 0;
let newVersion: number;
if (direction === "prev") {
newVersion = currentVersion > 0 ? currentVersion - 1 : message.versions.length - 1;
} else {
newVersion = currentVersion < message.versions.length - 1 ? currentVersion + 1 : 0;
}
const updatedMessage: ChatMessage = {
...message,
content: message.versions[newVersion].content,
activeVersion: newVersion,
};
const updatedMessages = [...session.messages];
updatedMessages[messageIndex] = updatedMessage;
setSession({ ...session, messages: updatedMessages });
};
// Функции управления сессиями
const handleCreateNewSession = async () => {
if (!story || !id) return;
@@ -552,15 +693,74 @@ export default function GamePage() {
>
{session?.messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
<div className="message-content">
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
<span className="message-time">
{new Date(message.timestamp).toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
{editingMessageId === message.id ? (
<div className="message-edit-form">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="message-edit-textarea"
autoFocus
/>
<div className="message-edit-actions">
<button
className="edit-cancel-btn"
onClick={handleCancelEdit}
>
Отмена
</button>
<button
className="edit-save-btn"
onClick={() => handleSaveEdit(message.id)}
>
Сохранить и переиграть
</button>
</div>
</div>
) : (
<>
<div className="message-content">
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
<div className="message-footer">
<span className="message-time">
{new Date(message.timestamp).toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
{message.role === "user" && !isLoading && (
<div className="message-actions">
{message.versions && message.versions.length > 1 && (
<div className="version-switcher">
<button
className="version-btn"
onClick={() => handleSwitchVersion(message.id, "prev")}
>
</button>
<span className="version-indicator">
{(message.activeVersion || 0) + 1}/{message.versions.length}
</span>
<button
className="version-btn"
onClick={() => handleSwitchVersion(message.id, "next")}
>
</button>
</div>
)}
<button
className="edit-btn"
onClick={() => handleEditMessage(message.id, message.content)}
title="Редактировать"
>
</button>
</div>
)}
</div>
</>
)}
</div>
))}