Major refactor: security, performance, and code organization

Security:
- DeepSeek API moved to server-side proxy with rate limiting (20 req/min)
- Whitelist validation for all POST/PUT routes
- Cookie security (secure, sameSite, httpOnly in production)
- Input validation for messages, tokens, temperature
- Sanitized hasOwnProperty to prevent prototype pollution

Performance:
- Lazy loading for chat messages (sliding window of 20)
- Streaming response throttling (50ms batches)
- Scroll optimization (only scroll on new messages)
- AbortController fix for stop button

Code organization:
- GamePage refactored from ~1170 to ~750 lines
- New hooks: useGameSession, useStreamingResponse, useCharacterDetection, useLazyMessages
- New components: MessageList, ChatInput, SessionSelector, CharacterPanel
- Fixed ESLint errors

Features:
- OOC mode button for direct AI instructions
- Message versions (aiResponse) now persist to DB
- playerId saved in sessions
This commit is contained in:
Alexej Wolff
2026-05-05 23:41:52 +02:00
parent bbefa114f8
commit 68c2b129fa
23 changed files with 1817 additions and 553 deletions
+208
View File
@@ -0,0 +1,208 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import type { ChatMessage } from "../../types";
interface MessageItemProps {
message: ChatMessage;
isEditing: boolean;
editContent: string;
isLoading: boolean;
onEditContentChange: (content: string) => void;
onEditMessage: (messageId: string, content: string) => void;
onCancelEdit: () => void;
onSaveEdit: (messageId: string) => void;
onSwitchVersion: (messageId: string, direction: "prev" | "next") => void;
}
export function MessageItem({
message,
isEditing,
editContent,
isLoading,
onEditContentChange,
onEditMessage,
onCancelEdit,
onSaveEdit,
onSwitchVersion,
}: MessageItemProps) {
if (isEditing) {
return (
<div className={`message ${message.role}`}>
<div className="message-edit-form">
<textarea
value={editContent}
onChange={(e) => onEditContentChange(e.target.value)}
className="message-edit-textarea"
autoFocus
/>
<div className="message-edit-actions">
<button className="edit-cancel-btn" onClick={onCancelEdit}>
Отмена
</button>
<button
className="edit-save-btn"
onClick={() => onSaveEdit(message.id)}
>
Сохранить и переиграть
</button>
</div>
</div>
</div>
);
}
return (
<div className={`message ${message.role}`}>
<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={() => onSwitchVersion(message.id, "prev")}
>
</button>
<span className="version-indicator">
{(message.activeVersion || 0) + 1}/{message.versions.length}
</span>
<button
className="version-btn"
onClick={() => onSwitchVersion(message.id, "next")}
>
</button>
</div>
)}
<button
className="edit-btn"
onClick={() => onEditMessage(message.id, message.content)}
title="Редактировать"
>
</button>
</div>
)}
</div>
</div>
);
}
interface StreamingMessageProps {
content: string;
}
export function StreamingMessage({ content }: StreamingMessageProps) {
return (
<div className="message assistant streaming">
<div className="message-content">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
</div>
);
}
export function LoadingMessage() {
return (
<div className="message assistant loading">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
);
}
interface ErrorMessageProps {
error: string;
onDismiss: () => void;
}
export function ErrorMessage({ error, onDismiss }: ErrorMessageProps) {
return (
<div className="error-message">
<span> {error}</span>
<button onClick={onDismiss}></button>
</div>
);
}
interface MessageListProps {
messages: ChatMessage[];
streamingContent: string;
isLoading: boolean;
error: string | null;
editingMessageId: string | null;
editContent: string;
onEditContentChange: (content: string) => void;
onEditMessage: (messageId: string, content: string) => void;
onCancelEdit: () => void;
onSaveEdit: (messageId: string) => void;
onSwitchVersion: (messageId: string, direction: "prev" | "next") => void;
onDismissError: () => void;
messagesEndRef: React.RefObject<HTMLDivElement>;
messagesContainerRef: React.RefObject<HTMLDivElement>;
onScroll: () => void;
}
export function MessageList({
messages,
streamingContent,
isLoading,
error,
editingMessageId,
editContent,
onEditContentChange,
onEditMessage,
onCancelEdit,
onSaveEdit,
onSwitchVersion,
onDismissError,
messagesEndRef,
messagesContainerRef,
onScroll,
}: MessageListProps) {
return (
<div
className="messages-container"
ref={messagesContainerRef}
onScroll={onScroll}
>
{messages.map((message) => (
<MessageItem
key={message.id}
message={message}
isEditing={editingMessageId === message.id}
editContent={editContent}
isLoading={isLoading}
onEditContentChange={onEditContentChange}
onEditMessage={onEditMessage}
onCancelEdit={onCancelEdit}
onSaveEdit={onSaveEdit}
onSwitchVersion={onSwitchVersion}
/>
))}
{isLoading && streamingContent && (
<StreamingMessage content={streamingContent} />
)}
{isLoading && !streamingContent && <LoadingMessage />}
{error && <ErrorMessage error={error} onDismiss={onDismissError} />}
<div ref={messagesEndRef} />
</div>
);
}