172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { MessageCircle, X, GripHorizontal } from 'lucide-react';
|
|
import ChatMessage from './ChatMessage';
|
|
import ChatInput from './ChatInput';
|
|
import useScreenSize from '../../hooks/useScreenSize';
|
|
import { useWebSocketContext } from '../../contexts/WebSocketContext';
|
|
import { api } from '../../services/api';
|
|
|
|
interface ChatWindowProps {
|
|
className?: string;
|
|
}
|
|
|
|
const ChatWindow: React.FC<ChatWindowProps> = ({ className = '' }) => {
|
|
const screenSize = useScreenSize();
|
|
const [chatOpen, setChatOpen] = useState(false);
|
|
const [chatHeight, setChatHeight] = useState(400);
|
|
const [chatWidth, setChatWidth] = useState(360);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
const startHeightRef = useRef<number>(0);
|
|
const startWidthRef = useRef<number>(0);
|
|
const startPosRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 });
|
|
|
|
const { messages = [], isConnected } = useWebSocketContext();
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages]);
|
|
|
|
const handleResizeStart = (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
startHeightRef.current = chatHeight;
|
|
startWidthRef.current = chatWidth;
|
|
startPosRef.current = { x: e.clientX, y: e.clientY };
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!isDragging) return;
|
|
|
|
const deltaY = startPosRef.current.y - e.clientY;
|
|
const deltaX = startPosRef.current.x - e.clientX;
|
|
|
|
const newHeight = Math.max(300, startHeightRef.current + deltaY);
|
|
const newWidth = Math.max(300, startWidthRef.current + deltaX);
|
|
|
|
setChatHeight(newHeight);
|
|
setChatWidth(newWidth);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
};
|
|
|
|
if (isDragging) {
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
}, [isDragging]);
|
|
|
|
const handleSendMessage = async (content: string) => {
|
|
try {
|
|
setIsLoading(true);
|
|
await api.addChatMessage({ message: content });
|
|
} catch (error) {
|
|
console.error('Failed to send message:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteMessage = async (id: string) => {
|
|
try {
|
|
await api.deleteChatMessage(id);
|
|
} catch (error) {
|
|
console.error('Failed to delete message:', error);
|
|
}
|
|
};
|
|
|
|
if (screenSize === 'mobile') {
|
|
return (
|
|
<div className={`h-full flex flex-col bg-white ${className}`}>
|
|
<div className="px-4 py-2 border-b bg-gray-50 flex justify-between items-center">
|
|
<h3 className="font-semibold text-sm">Chat</h3>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{messages.length === 0 ? (
|
|
<div className="flex items-center justify-center h-full text-gray-400">
|
|
<p>No messages yet</p>
|
|
</div>
|
|
) : (
|
|
messages.map((message) => (
|
|
<ChatMessage
|
|
key={message.id}
|
|
{...message}
|
|
onDelete={handleDeleteMessage}
|
|
/>
|
|
))
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
<ChatInput onSendMessage={handleSendMessage} disabled={isLoading || !isConnected} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!chatOpen && (
|
|
<button
|
|
onClick={() => setChatOpen(true)}
|
|
className="fixed bottom-4 right-4 w-14 h-14 bg-blue-500 text-white rounded-full shadow-lg hover:bg-blue-600 flex items-center justify-center z-40"
|
|
aria-label="Open chat"
|
|
>
|
|
<MessageCircle className="w-6 h-6" />
|
|
</button>
|
|
)}
|
|
|
|
{chatOpen && (
|
|
<div
|
|
className="fixed bottom-0 right-4 bg-white shadow-2xl rounded-t-lg flex flex-col z-40"
|
|
style={{ height: `${chatHeight}px`, width: `${chatWidth}px` }}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between p-3 border-b bg-gray-50 rounded-t-lg cursor-move"
|
|
onMouseDown={handleResizeStart}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<GripHorizontal className="w-4 h-4 text-gray-400" />
|
|
<h3 className="font-semibold text-sm">Chat</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => setChatOpen(false)}
|
|
className="text-gray-500 hover:text-gray-700 p-1"
|
|
aria-label="Close chat"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{messages.length === 0 ? (
|
|
<div className="flex items-center justify-center h-full text-gray-400">
|
|
<p>No messages yet</p>
|
|
</div>
|
|
) : (
|
|
messages.map((message) => (
|
|
<ChatMessage
|
|
key={message.id}
|
|
{...message}
|
|
onDelete={handleDeleteMessage}
|
|
/>
|
|
))
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
<ChatInput onSendMessage={handleSendMessage} disabled={isLoading || !isConnected} />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ChatWindow; |