New web interface, move llm engine to separate process
This commit is contained in:
172
web/src/components/Chat/ChatWindow.tsx
Normal file
172
web/src/components/Chat/ChatWindow.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user