Files
ReSekai/src/components/game/MessageList.tsx
T
Alexej Wolff 68c2b129fa 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
2026-05-05 23:41:52 +02:00

209 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}