New frontend and tools
This commit is contained in:
133
dewey/components/ChatArea.tsx
Normal file
133
dewey/components/ChatArea.tsx
Normal 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>
|
||||
)
|
||||
}
|
85
dewey/components/ChatContainer.tsx
Normal file
85
dewey/components/ChatContainer.tsx
Normal 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;
|
48
dewey/components/ChatTabs.tsx
Normal file
48
dewey/components/ChatTabs.tsx
Normal 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;
|
129
dewey/components/Sidebar.tsx
Normal file
129
dewey/components/Sidebar.tsx
Normal 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;
|
37
dewey/components/UserInput.tsx
Normal file
37
dewey/components/UserInput.tsx
Normal 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;
|
Reference in New Issue
Block a user