New frontend and tools

This commit is contained in:
2024-10-01 19:31:57 -04:00
parent b56619a2e6
commit 9d25dd7d94
15 changed files with 937 additions and 191 deletions

View File

@ -0,0 +1,133 @@
import { useState, useEffect } from 'react'
import ChatTabs from './ChatTabs'
import ChatContainer from './ChatContainer'
import UserInput from './UserInput'
export default function ChatArea({ currentChatId, setCurrentChatId, chats, setChats, createNewChat, socket }) {
const [userInput, setUserInput] = useState('')
const sendMessage = () => {
if (userInput.trim() && currentChatId) {
const newMessage = { content: userInput, isUser: true }
setChats(prevChats => ({
...prevChats,
[currentChatId]: {
...prevChats[currentChatId],
messages: [...prevChats[currentChatId].messages, newMessage],
thinkingSections: [...prevChats[currentChatId].thinkingSections, { thoughts: [] }]
}
}))
socket.emit('chat_request', {
message: userInput,
conversation_history: chats[currentChatId].messages
.filter(m => !m.isUser).map(m => ({ role: 'assistant', content: m.content }))
.concat(chats[currentChatId].messages.filter(m => m.isUser).map(m => ({ role: 'user', content: m.content })))
})
setUserInput('')
}
}
const switchToChat = (chatId: string) => {
setCurrentChatId(chatId);
}
const closeChat = (chatId: string) => {
if (window.confirm('Are you sure you want to close this chat?')) {
setChats(prevChats => {
const newChats = { ...prevChats };
delete newChats[chatId];
return newChats;
});
if (currentChatId === chatId) {
const remainingChatIds = Object.keys(chats).filter(id => id !== chatId);
if (remainingChatIds.length > 0) {
switchToChat(remainingChatIds[0]);
} else {
createNewChat();
}
}
}
}
useEffect(() => {
if (socket) {
socket.on('thinking', (data) => {
// Handle thinking event
setChats(prevChats => ({
...prevChats,
[currentChatId]: {
...prevChats[currentChatId],
thinkingSections: [
...prevChats[currentChatId].thinkingSections,
{ thoughts: [{ type: 'thinking', content: data.step }] }
]
}
}));
});
socket.on('thought', (data) => {
// Handle thought event
setChats(prevChats => ({
...prevChats,
[currentChatId]: {
...prevChats[currentChatId],
thinkingSections: prevChats[currentChatId].thinkingSections.map((section, index) =>
index === prevChats[currentChatId].thinkingSections.length - 1
? { ...section, thoughts: [...section.thoughts, data] }
: section
)
}
}));
});
socket.on('chat_response', (data) => {
// Handle chat response event
setChats(prevChats => ({
...prevChats,
[currentChatId]: {
...prevChats[currentChatId],
messages: [...prevChats[currentChatId].messages, { content: data.response, isUser: false }]
}
}));
});
socket.on('error', (data) => {
// Handle error event
console.error('Error:', data.message);
// You might want to display this error to the user
});
}
return () => {
if (socket) {
socket.off('thinking');
socket.off('thought');
socket.off('chat_response');
socket.off('error');
}
};
}, [socket, currentChatId, setChats]);
return (
<div className="flex flex-col flex-1">
<ChatTabs
chats={chats}
currentChatId={currentChatId}
createNewChat={createNewChat}
switchToChat={switchToChat}
closeChat={closeChat}
/>
{currentChatId && (
<ChatContainer
currentChat={chats[currentChatId]}
socket={socket}
/>
)}
<UserInput
value={userInput}
onChange={setUserInput}
onSend={sendMessage}
/>
</div>
)
}

View File

@ -0,0 +1,85 @@
import React, { useEffect, useRef } from 'react';
import { marked } from 'marked';
interface ChatContainerProps {
currentChat: {
messages: Array<{ content: string; isUser: boolean }>;
thinkingSections: Array<{ thoughts: Array<{ type: string; content: string; details?: string }> }>;
} | null;
socket: any;
}
const ChatContainer: React.FC<ChatContainerProps> = ({ currentChat, socket }) => {
const chatContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [currentChat]);
if (!currentChat) return null;
return (
<div ref={chatContainerRef} className="flex-1 overflow-y-auto p-4 bg-gray-900">
{currentChat.messages.map((message, index) => (
<div
key={index}
className={`mb-4 ${
message.isUser ? 'text-right text-cyan-300' : 'text-left text-white'
}`}
>
<div
className={`inline-block p-2 rounded-lg ${
message.isUser ? 'bg-cyan-800' : 'bg-gray-700'
}`}
>
{message.isUser ? (
message.content
) : (
<div dangerouslySetInnerHTML={{ __html: marked(message.content) }} />
)}
</div>
</div>
))}
{currentChat.thinkingSections.map((section, sectionIndex) => (
<div key={sectionIndex} className="mb-4 border-l-2 border-gray-600 pl-4">
{section.thoughts.map((thought, thoughtIndex) => (
<div key={thoughtIndex} className="mb-2">
<div className={`font-bold ${getThoughtColor(thought.type)}`}>
{thought.type}:
</div>
<div dangerouslySetInnerHTML={{ __html: marked(thought.content) }} />
{thought.details && (
<pre className="mt-2 p-2 bg-gray-800 rounded">
{thought.details}
</pre>
)}
</div>
))}
</div>
))}
</div>
);
};
function getThoughtColor(type: string): string {
switch (type.toLowerCase()) {
case 'plan':
return 'text-blue-400';
case 'decision':
return 'text-green-400';
case 'tool_call':
return 'text-yellow-400';
case 'tool_result':
return 'text-purple-400';
case 'think_more':
return 'text-pink-400';
case 'answer':
return 'text-red-400';
default:
return 'text-gray-400';
}
}
export default ChatContainer;

View File

@ -0,0 +1,48 @@
import React from 'react';
interface ChatTabsProps {
chats: Record<string, any>;
currentChatId: string | null;
createNewChat: () => void;
switchToChat: (chatId: string) => void;
closeChat: (chatId: string) => void;
}
const ChatTabs: React.FC<ChatTabsProps> = ({ chats, currentChatId, createNewChat, switchToChat, closeChat }) => {
return (
<div className="flex bg-gray-800 p-2">
{Object.keys(chats).map((chatId) => (
<div
key={chatId}
className={`px-4 py-2 mr-2 rounded-t-lg flex items-center ${
chatId === currentChatId ? 'bg-gray-600' : 'bg-gray-700'
}`}
>
<button
onClick={() => switchToChat(chatId)}
className="flex-grow text-left"
>
Chat {chatId}
</button>
<button
className="ml-2 text-red-500 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
closeChat(chatId);
}}
>
×
</button>
</div>
))}
<button
className="px-4 py-2 bg-green-600 rounded-t-lg"
onClick={createNewChat}
>
+ New Chat
</button>
</div>
);
};
export default ChatTabs;

View File

@ -0,0 +1,129 @@
import React, { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('chart.js/auto').then((mod) => mod.Chart), {
ssr: false,
});
interface SidebarProps {
socket: any;
}
const Sidebar: React.FC<SidebarProps> = ({ socket }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const chartRefs = useRef<{ [key: string]: any }>({
cpu: null,
memory: null,
disk: null,
gpu: null,
gpuMemory: null,
});
useEffect(() => {
if (socket) {
socket.on('system_resources', (data: any) => {
updateCharts(data);
});
}
return () => {
if (socket) {
socket.off('system_resources');
}
};
}, [socket]);
useEffect(() => {
const initCharts = async () => {
const ChartJS = await Chart;
initializeCharts(ChartJS);
};
initCharts();
return () => {
Object.values(chartRefs.current).forEach(chart => chart?.destroy());
};
}, []);
const initializeCharts = (ChartJS: any) => {
const chartConfig = {
type: 'line',
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: {
unit: 'second',
},
},
y: {
beginAtZero: true,
max: 100,
},
},
animation: false,
},
data: {
datasets: [{
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
}],
},
};
['cpu', 'memory', 'disk', 'gpu', 'gpuMemory'].forEach(chartName => {
const ctx = document.getElementById(`${chartName}Chart`) as HTMLCanvasElement;
if (ctx) {
chartRefs.current[chartName] = new ChartJS(ctx, chartConfig);
}
});
};
const updateCharts = (data: any) => {
const now = new Date();
Object.entries(data).forEach(([key, value]) => {
const chartName = key.replace('_', '').toLowerCase();
const chart = chartRefs.current[chartName];
if (chart) {
chart.data.datasets[0].data.push({x: now, y: value});
chart.update('none');
}
});
};
return (
<div className={`w-80 bg-gray-800 p-4 ${isCollapsed ? 'hidden' : ''}`}>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="mb-4 px-4 py-2 bg-gray-700 text-white rounded-lg"
>
{isCollapsed ? 'Show Charts' : 'Hide Charts'}
</button>
<div className="mb-4">
<h3 className="text-white mb-2">CPU Load</h3>
<canvas id="cpuChart"></canvas>
</div>
<div className="mb-4">
<h3 className="text-white mb-2">Memory Usage</h3>
<canvas id="memoryChart"></canvas>
</div>
<div className="mb-4">
<h3 className="text-white mb-2">Disk I/O</h3>
<canvas id="diskChart"></canvas>
</div>
<div className="mb-4">
<h3 className="text-white mb-2">GPU Load</h3>
<canvas id="gpuChart"></canvas>
</div>
<div className="mb-4">
<h3 className="text-white mb-2">GPU Memory</h3>
<canvas id="gpuMemoryChart"></canvas>
</div>
</div>
);
};
export default Sidebar;

View File

@ -0,0 +1,37 @@
import React from 'react';
interface UserInputProps {
value: string;
onChange: (value: string) => void;
onSend: () => void;
}
const UserInput: React.FC<UserInputProps> = ({ value, onChange, onSend }) => {
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
};
return (
<div className="p-4 bg-gray-800">
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyPress={handleKeyPress}
className="w-full p-2 bg-gray-700 text-white rounded-lg resize-none"
rows={3}
placeholder="Type your message here..."
/>
<button
onClick={onSend}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Send
</button>
</div>
);
};
export default UserInput;