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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user