68c2b129fa
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
209 lines
5.5 KiB
TypeScript
209 lines
5.5 KiB
TypeScript
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>
|
||
);
|
||
}
|