diff --git a/src/pages/GamePage.css b/src/pages/GamePage.css index c1a91a6..d20c4f7 100644 --- a/src/pages/GamePage.css +++ b/src/pages/GamePage.css @@ -307,10 +307,10 @@ } .message.assistant .message-content { - background: #1a1a1a; + background: transparent; color: #e5e5e5; border: none; - border-bottom-left-radius: 6px; + padding: 0.5rem 0; } .message-content p { @@ -364,19 +364,144 @@ margin: 0.75rem 0; } -.message-time { - display: block; - font-size: 0.65rem; - color: #444; +.message-footer { + display: flex; + align-items: center; + justify-content: space-between; margin-top: 0.25rem; padding: 0 0.4rem; } +.message-time { + font-size: 0.65rem; + color: #444; +} + .message.user .message-time { - text-align: right; color: rgba(255, 255, 255, 0.4); } +.message.user .message-footer { + flex-direction: row-reverse; +} + +.message-actions { + display: flex; + align-items: center; + gap: 0.5rem; + opacity: 0; + transition: opacity 0.2s; +} + +.message:hover .message-actions { + opacity: 1; +} + +.edit-btn { + background: none; + border: none; + font-size: 0.75rem; + cursor: pointer; + padding: 0.25rem; + opacity: 0.6; + transition: opacity 0.2s; +} + +.edit-btn:hover { + opacity: 1; +} + +.version-switcher { + display: flex; + align-items: center; + gap: 0.25rem; + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 0.15rem 0.35rem; +} + +.version-btn { + background: none; + border: none; + color: #888; + font-size: 0.9rem; + cursor: pointer; + padding: 0 0.25rem; + line-height: 1; + transition: color 0.2s; +} + +.version-btn:hover { + color: #fff; +} + +.version-indicator { + font-size: 0.7rem; + color: #666; + min-width: 2rem; + text-align: center; +} + +/* Message editing */ +.message-edit-form { + width: 100%; +} + +.message-edit-textarea { + width: 100%; + min-height: 60px; + padding: 0.75rem; + background: #1a1a1a; + border: 1px solid rgba(37, 99, 235, 0.5); + border-radius: 12px; + color: white; + font-size: 0.95rem; + font-family: inherit; + resize: vertical; + line-height: 1.4; +} + +.message-edit-textarea:focus { + outline: none; + border-color: #2563eb; +} + +.message-edit-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.edit-cancel-btn, +.edit-save-btn { + padding: 0.4rem 0.75rem; + border: none; + border-radius: 8px; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.edit-cancel-btn { + background: #333; + color: #aaa; +} + +.edit-cancel-btn:hover { + background: #444; + color: #fff; +} + +.edit-save-btn { + background: #2563eb; + color: white; +} + +.edit-save-btn:hover { + background: #1d4ed8; +} + .message.loading .message-content { padding: 1rem; } @@ -664,4 +789,18 @@ right: 0.75rem; font-size: 1rem; } + + .message-actions { + opacity: 1; + } + + .message-edit-textarea { + font-size: 0.9rem; + } + + .edit-cancel-btn, + .edit-save-btn { + font-size: 0.75rem; + padding: 0.35rem 0.6rem; + } } diff --git a/src/pages/GamePage.tsx b/src/pages/GamePage.tsx index 8e9e2c1..d8c2334 100644 --- a/src/pages/GamePage.tsx +++ b/src/pages/GamePage.tsx @@ -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(null); + const [editContent, setEditContent] = useState(""); const abortControllerRef = useRef(null); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(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) => (
-
- {message.content} -
- - {new Date(message.timestamp).toLocaleTimeString("ru-RU", { - hour: "2-digit", - minute: "2-digit", - })} - + {editingMessageId === message.id ? ( +
+