first commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "reason-flow-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"axios": "^1.6.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-syntax-highlighter": "^5.8.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"styled-components": "^6.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Reason Flow - Advanced Engineering Reasoning System"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Reason Flow - Engineering AI</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Reason Flow",
|
||||
"name": "Reason Flow - Engineering AI",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3b82f6",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import ChatPage from './pages/ChatPage';
|
||||
import DocumentsPage from './pages/DocumentsPage';
|
||||
import ToolsPage from './pages/ToolsPage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import Layout from './components/Layout';
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '2rem',
|
||||
height: '2rem',
|
||||
border: '3px solid #e2e8f0',
|
||||
borderTop: '3px solid #3b82f6',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite'
|
||||
}}></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <Layout>{children}</Layout>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/dashboard" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/chat" element={
|
||||
<ProtectedRoute>
|
||||
<ChatPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/documents" element={
|
||||
<ProtectedRoute>
|
||||
<DocumentsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/tools" element={
|
||||
<ProtectedRoute>
|
||||
<ToolsPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/admin" element={
|
||||
<ProtectedRoute>
|
||||
<AdminPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,283 @@
|
||||
/* Layout Styles */
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
left: -280px;
|
||||
transition: left 0.3s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cbd5e1;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.125rem;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #b91c1c;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Top Bar */
|
||||
.topbar {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-toggle:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Page Content */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Mobile Overlay */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.menu-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Layout.css';
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{ path: '/dashboard', label: 'Dashboard', icon: '🏠' },
|
||||
{ path: '/chat', label: 'Engineering Chat', icon: '💬' },
|
||||
{ path: '/documents', label: 'Knowledge Base', icon: '📚' },
|
||||
{ path: '/tools', label: 'Tool Execution', icon: '🔧' },
|
||||
...(user?.role === 'admin' ? [{ path: '/admin', label: 'Admin Panel', icon: '⚙️' }] : [])
|
||||
];
|
||||
|
||||
const isActive = (path) => location.pathname === path;
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
{/* Sidebar */}
|
||||
<aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
|
||||
<div className="sidebar-header">
|
||||
<div className="logo">
|
||||
<span className="logo-icon">🧠</span>
|
||||
<span className="logo-text">Reason Flow</span>
|
||||
</div>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
key={item.path}
|
||||
className={`nav-item ${isActive(item.path) ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
navigate(item.path);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="nav-icon">{item.icon}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<div className="user-info">
|
||||
<div className="user-avatar">
|
||||
{user?.firstName?.[0]}{user?.lastName?.[0]}
|
||||
</div>
|
||||
<div className="user-details">
|
||||
<div className="user-name">{user?.firstName} {user?.lastName}</div>
|
||||
<div className="user-role">{user?.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="logout-btn" onClick={handleLogout}>
|
||||
🚪 Logout
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="main-content">
|
||||
{/* Top Bar */}
|
||||
<header className="topbar">
|
||||
<button
|
||||
className="menu-toggle"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<div className="topbar-title">
|
||||
{navigation.find(item => item.path === location.pathname)?.label || 'Reason Flow'}
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<div className="status-indicator">
|
||||
<span className="status-dot"></span>
|
||||
System Online
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="page-content">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile Overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="mobile-overlay"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
// Verify token and get user info
|
||||
axios.get('/api/auth/profile')
|
||||
.then(response => {
|
||||
setUser(response.data.data.user);
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem('token');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', { email, password });
|
||||
const { token, user } = response.data.data;
|
||||
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
setUser(user);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || 'Login failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (userData) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/register', userData);
|
||||
const { token, user } = response.data.data;
|
||||
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
setUser(user);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || 'Registration failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
delete axios.defaults.headers.common['Authorization'];
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
loading
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f8fafc;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* Reset and base styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
*:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Scrollbar styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,515 @@
|
||||
/* Admin Page Styles */
|
||||
.admin-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Admin Section */
|
||||
.admin-section {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Model Status */
|
||||
.model-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.model-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.model-header h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-indicator.unknown {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.model-card p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.model-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Feedback Management */
|
||||
.feedback-overview {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.stat-value.positive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-value.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-value.correction {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-value.suggestion {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Recent Feedback */
|
||||
.recent-feedback h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feedback-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feedback-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feedback-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.feedback-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feedback-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.type-badge.positive {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.type-badge.negative {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.type-badge.correction {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.type-badge.suggestion {
|
||||
background: #e9d5ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.feedback-rating {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.feedback-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.process-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.process-btn:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.process-btn:disabled {
|
||||
background: #10b981;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.feedback-comment,
|
||||
.feedback-correction {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feedback-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* System Actions */
|
||||
.system-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.action-group h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: #e2e8f0;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.model-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.system-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.feedback-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './AdminPage.css';
|
||||
|
||||
const AdminPage = () => {
|
||||
const [systemStats, setSystemStats] = useState({
|
||||
users: 0,
|
||||
conversations: 0,
|
||||
documents: 0,
|
||||
feedback: 0,
|
||||
toolExecutions: 0
|
||||
});
|
||||
const [modelStatus, setModelStatus] = useState({});
|
||||
const [feedbackStats, setFeedbackStats] = useState({});
|
||||
const [recentFeedback, setRecentFeedback] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadAdminData();
|
||||
}, []);
|
||||
|
||||
const loadAdminData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load system statistics
|
||||
const [conversationsRes, documentsRes, feedbackRes, toolsRes, modelRes] = await Promise.all([
|
||||
axios.get('/api/chat/conversations?limit=1'),
|
||||
axios.get('/api/documents?limit=1'),
|
||||
axios.get('/api/feedback/stats'),
|
||||
axios.get('/api/tools/executions?limit=1'),
|
||||
axios.get('/api/models/status')
|
||||
]);
|
||||
|
||||
setSystemStats({
|
||||
users: 0, // Would need a users endpoint
|
||||
conversations: conversationsRes.data.data.pagination?.total || 0,
|
||||
documents: documentsRes.data.data.pagination?.total || documentsRes.data.data.documents?.length || 0,
|
||||
feedback: feedbackRes.data.data.total || 0,
|
||||
toolExecutions: toolsRes.data.data.pagination?.total || toolsRes.data.data.executions?.length || 0
|
||||
});
|
||||
|
||||
setModelStatus(modelRes.data.data);
|
||||
setFeedbackStats(feedbackRes.data.data);
|
||||
|
||||
// Load recent feedback
|
||||
const feedbackListRes = await axios.get('/api/feedback?limit=5');
|
||||
setRecentFeedback(feedbackListRes.data.data.feedback || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading admin data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processFeedback = async (feedbackId) => {
|
||||
try {
|
||||
await axios.put(`/api/feedback/${feedbackId}/process`);
|
||||
setMessage('✅ Feedback processed successfully');
|
||||
loadAdminData();
|
||||
} catch (error) {
|
||||
console.error('Error processing feedback:', error);
|
||||
setMessage(`❌ Failed to process feedback: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading admin dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="page-header">
|
||||
<h1>⚙️ Admin Panel</h1>
|
||||
<p>System administration and monitoring</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Overview */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>📊 System Overview</h2>
|
||||
<p>Key system metrics and statistics</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">👥</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.users}</div>
|
||||
<div className="stat-label">Total Users</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💬</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.conversations}</div>
|
||||
<div className="stat-label">Conversations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📚</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.documents}</div>
|
||||
<div className="stat-label">Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">⭐</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.feedback}</div>
|
||||
<div className="stat-label">Feedback Items</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">🔧</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{systemStats.toolExecutions}</div>
|
||||
<div className="stat-label">Tool Executions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Status */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>🤖 Model Status</h2>
|
||||
<p>AI model health and performance</p>
|
||||
</div>
|
||||
|
||||
<div className="model-status-grid">
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>🧠 MODEL1 (Planner)</h3>
|
||||
<span className={`status-indicator ${modelStatus.model1 || 'unknown'}`}>
|
||||
{modelStatus.model1 || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Engineering plan generation and reasoning</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.model1 || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>⚡ QUERYMODEL (Executor)</h3>
|
||||
<span className={`status-indicator ${modelStatus.queryModel || 'unknown'}`}>
|
||||
{modelStatus.queryModel || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Plan execution and tool orchestration</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.queryModel || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="model-card">
|
||||
<div className="model-header">
|
||||
<h3>🔍 RAG System</h3>
|
||||
<span className={`status-indicator ${modelStatus.rag || 'unknown'}`}>
|
||||
{modelStatus.rag || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<p>Document search and knowledge retrieval</p>
|
||||
<div className="model-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Status:</span>
|
||||
<span className="metric-value">{modelStatus.rag || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Management */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>💬 Feedback Management</h2>
|
||||
<p>User feedback and system improvements</p>
|
||||
</div>
|
||||
|
||||
<div className="feedback-overview">
|
||||
<div className="feedback-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Total Feedback:</span>
|
||||
<span className="stat-value">{feedbackStats.total || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Positive:</span>
|
||||
<span className="stat-value positive">{feedbackStats.positive || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Negative:</span>
|
||||
<span className="stat-value negative">{feedbackStats.negative || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Corrections:</span>
|
||||
<span className="stat-value correction">{feedbackStats.correction || 0}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Suggestions:</span>
|
||||
<span className="stat-value suggestion">{feedbackStats.suggestion || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recentFeedback.length > 0 && (
|
||||
<div className="recent-feedback">
|
||||
<h3>Recent Feedback</h3>
|
||||
<div className="feedback-list">
|
||||
{recentFeedback.map((feedback) => (
|
||||
<div key={feedback.id} className="feedback-item">
|
||||
<div className="feedback-header">
|
||||
<div className="feedback-type">
|
||||
<span className={`type-badge ${feedback.feedback_type}`}>
|
||||
{feedback.feedback_type}
|
||||
</span>
|
||||
<span className="feedback-rating">
|
||||
{feedback.rating ? `⭐ ${feedback.rating}` : 'No rating'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="feedback-actions">
|
||||
<button
|
||||
className="process-btn"
|
||||
onClick={() => processFeedback(feedback.id)}
|
||||
disabled={feedback.is_processed}
|
||||
>
|
||||
{feedback.is_processed ? '✅ Processed' : '🔄 Process'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{feedback.comment && (
|
||||
<div className="feedback-comment">
|
||||
<strong>Comment:</strong> {feedback.comment}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feedback.corrected_content && (
|
||||
<div className="feedback-correction">
|
||||
<strong>Correction:</strong> {feedback.corrected_content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="feedback-meta">
|
||||
<span>ID: {feedback.id}</span>
|
||||
<span>Date: {new Date(feedback.created_at).toLocaleDateString()}</span>
|
||||
<span>Status: {feedback.is_processed ? 'Processed' : 'Pending'}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Actions */}
|
||||
<div className="admin-section">
|
||||
<div className="section-header">
|
||||
<h2>🛠️ System Actions</h2>
|
||||
<p>Administrative operations and maintenance</p>
|
||||
</div>
|
||||
|
||||
<div className="system-actions">
|
||||
<div className="action-group">
|
||||
<h3>Data Management</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
📊 Export Data
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🗑️ Cleanup Old Data
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔄 Refresh Cache
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-group">
|
||||
<h3>Model Management</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
🚀 Retrain Models
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
📈 Update Metrics
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔧 Test Models
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-group">
|
||||
<h3>System Maintenance</h3>
|
||||
<div className="action-buttons">
|
||||
<button className="action-btn primary">
|
||||
🔄 Restart Services
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
📋 Generate Report
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
🔍 Run Diagnostics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
@@ -0,0 +1,631 @@
|
||||
/* Chat Page Styles */
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Chat Header */
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #1e293b, #334155);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-title h2 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chat-title p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Pending Plans */
|
||||
.pending-plans {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pending-plans h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.plans-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.plan-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plan-status {
|
||||
background: #fbbf24;
|
||||
color: #92400e;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.plan-content pre {
|
||||
background: #f1f5f9;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #475569;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.plan-actions .btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.plan-actions .btn.approve {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn.reject {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn.execute {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plan-actions .btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Messages Container */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
align-self: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-role {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.feedback-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feedback-btn:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background: #f8fafc;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.message.user .message-text {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.message-text pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Message type specific styles */
|
||||
.message[data-message-type="system_prompt"] .message-content {
|
||||
background: #f0f9ff;
|
||||
border-left: 4px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.message[data-message-type="system_prompt"] .message-role {
|
||||
color: #0369a1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="plan"] .message-content {
|
||||
background: #fefce8;
|
||||
border-left: 4px solid #eab308;
|
||||
}
|
||||
|
||||
.message[data-message-type="plan"] .message-role {
|
||||
color: #a16207;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="execution_result"] .message-content {
|
||||
background: #f0fdf4;
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
|
||||
.message[data-message-type="execution_result"] .message-role {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message[data-message-type="error"] .message-content {
|
||||
background: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.message[data-message-type="error"] .message-role {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Plan Action Buttons */
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.approve-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reject-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.modify-btn {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modify-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
/* Typing Indicator */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
background: #64748b;
|
||||
border-radius: 50%;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
||||
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Chat Input */
|
||||
.chat-input {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-container textarea {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
transition: all 0.2s;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.input-container textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1.5rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-actions .btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-actions .btn.secondary {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.modal-actions .btn.primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-actions .btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
align-self: flex-end;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plan-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.chat-title h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.chat-title p {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import './ChatPage.css';
|
||||
|
||||
const ChatPage = () => {
|
||||
const [conversation, setConversation] = useState(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pendingPlans, setPendingPlans] = useState([]);
|
||||
const [showFeedback, setShowFeedback] = useState(null);
|
||||
const [feedbackData, setFeedbackData] = useState({
|
||||
feedbackType: 'positive',
|
||||
rating: 5,
|
||||
comment: '',
|
||||
correctedContent: ''
|
||||
});
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
createConversation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversation) {
|
||||
loadConversationMessages();
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const createConversation = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/conversations', {
|
||||
title: 'New Engineering Discussion'
|
||||
});
|
||||
setConversation(response.data.data.conversation);
|
||||
} catch (error) {
|
||||
console.error('Error creating conversation:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadConversationMessages = async () => {
|
||||
if (!conversation) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/chat/conversations/${conversation.id}`);
|
||||
const conv = response.data.data.conversation;
|
||||
|
||||
// Load all messages from the conversation
|
||||
const conversationMessages = conv.messages || [];
|
||||
setMessages(conversationMessages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: msg.created_at,
|
||||
message_type: msg.message_type,
|
||||
id: msg.id
|
||||
})));
|
||||
|
||||
// Load pending plans
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error loading conversation messages:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.trim() || !conversation) return;
|
||||
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: inputMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
const currentInput = inputMessage.trim().toLowerCase();
|
||||
setInputMessage('');
|
||||
setLoading(true);
|
||||
|
||||
// Do not locally infer approve/reject. Always send message to backend and let routing/LLM decide.
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/chat/message', {
|
||||
conversationId: conversation.id,
|
||||
content: inputMessage
|
||||
});
|
||||
|
||||
const assistantMessage = response.data.data.assistantMessage;
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
|
||||
// Check if this is a plan message and show approval prompt
|
||||
if (assistantMessage.message_type === 'plan') {
|
||||
loadPendingPlans();
|
||||
|
||||
// Add a system message prompting for approval
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `📋 **Engineering Plan Generated!** Please review the comprehensive plan above and choose your action:`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system_prompt',
|
||||
plan_id: assistantMessage.plan_id // Store the plan ID for the buttons
|
||||
}]);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: 'Sorry, I encountered an error. Please try again.',
|
||||
timestamp: new Date().toISOString()
|
||||
}]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlanApproval = async () => {
|
||||
if (pendingPlans.length === 0) {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '❌ No pending plans to approve. Please ask an engineering question first.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestPlan = pendingPlans[0];
|
||||
await approvePlan(latestPlan.id);
|
||||
};
|
||||
|
||||
const handlePlanRejection = async () => {
|
||||
if (pendingPlans.length === 0) {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '❌ No pending plans to reject. Please ask an engineering question first.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestPlan = pendingPlans[0];
|
||||
await rejectPlan(latestPlan.id);
|
||||
|
||||
// Ask for a new plan
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '🔄 **Plan Rejected!** Please provide more details or ask for a different approach to your engineering question.',
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system_prompt'
|
||||
}]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const loadPendingPlans = async () => {
|
||||
if (!conversation) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/chat/conversations/${conversation.id}`);
|
||||
const conv = response.data.data.conversation;
|
||||
|
||||
const plans = conv.messages
|
||||
.filter(m => m.message_type === 'plan' && m.plan_id)
|
||||
.map(m => ({
|
||||
id: m.plan_id,
|
||||
content: m.content,
|
||||
status: m.plan?.status || 'pending_approval',
|
||||
title: m.plan?.title || 'Generated Plan',
|
||||
created_at: m.created_at
|
||||
}))
|
||||
.filter(p => p.status === 'pending_approval' || p.status === 'draft');
|
||||
|
||||
setPendingPlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading pending plans:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const approvePlan = async (planId) => {
|
||||
try {
|
||||
await axios.put(`/api/models/model1/plan/${planId}`, {
|
||||
status: 'approved',
|
||||
approvalFeedback: 'Plan approved for execution'
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `✅ **Plan Approved!** The plan has been approved and is now executing with engineering tools...`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
|
||||
// Automatically execute the plan after approval
|
||||
setTimeout(() => {
|
||||
executePlan(planId);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error approving plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Approval Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectPlan = async (planId) => {
|
||||
try {
|
||||
await axios.put(`/api/models/model1/plan/${planId}`, {
|
||||
status: 'rejected',
|
||||
approvalFeedback: 'Plan rejected by user'
|
||||
});
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Plan Rejected!** The plan has been rejected. You can ask for a new plan or modify your request.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'system'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error rejecting plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Rejection Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const executePlan = async (planId) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.post('/api/models/querymodel/orchestrate', {
|
||||
planId,
|
||||
options: { topK: 5 }
|
||||
});
|
||||
|
||||
const result = response.data.data.toolExecution;
|
||||
|
||||
// Format the orchestration results nicely
|
||||
let formattedContent = `🚀 **Plan Executed Successfully!**\n\n`;
|
||||
|
||||
if (result.expandedQuery) {
|
||||
formattedContent += `**📋 Expanded Query:**\n${result.expandedQuery.substring(0, 500)}...\n\n`;
|
||||
}
|
||||
|
||||
if (result.extraction && result.extraction.results) {
|
||||
formattedContent += `**📚 Document Search Results:** ${result.extraction.results.length} documents found\n`;
|
||||
formattedContent += `**Confidence Score:** ${(result.extraction.confidence * 100).toFixed(1)}%\n\n`;
|
||||
}
|
||||
|
||||
if (result.web) {
|
||||
formattedContent += `**🌐 Web Search:** ${result.web.totalResults} results found\n`;
|
||||
formattedContent += `**Answer:** ${result.web.answer}\n\n`;
|
||||
}
|
||||
|
||||
if (result.report && result.report.content) {
|
||||
formattedContent += `**📊 Generated Report:**\n${result.report.content}\n\n`;
|
||||
}
|
||||
|
||||
// formattedContent += `**⚡ Processing Time:** ${result.report?.processingTime || 'N/A'}s\n`;
|
||||
// formattedContent += `**🔢 Tokens Used:** ${result.report?.tokensUsed || 'N/A'}`;
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: formattedContent,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'execution_result'
|
||||
}]);
|
||||
|
||||
loadPendingPlans();
|
||||
} catch (error) {
|
||||
console.error('Error executing plan:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `❌ **Execution Failed:** ${error.response?.data?.error || error.message}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_type: 'error'
|
||||
}]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitFeedback = async (messageId) => {
|
||||
try {
|
||||
await axios.post('/api/feedback/submit', {
|
||||
messageId,
|
||||
feedbackType: feedbackData.feedbackType,
|
||||
rating: feedbackData.rating,
|
||||
comment: feedbackData.comment,
|
||||
correctedContent: feedbackData.correctedContent
|
||||
});
|
||||
|
||||
setShowFeedback(null);
|
||||
setFeedbackData({
|
||||
feedbackType: 'positive',
|
||||
rating: 5,
|
||||
comment: '',
|
||||
correctedContent: ''
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-page">
|
||||
<div className="chat-container">
|
||||
{/* Chat Header */}
|
||||
<div className="chat-header">
|
||||
<div className="chat-title">
|
||||
<h2>🧠 Engineering Reasoning Chat</h2>
|
||||
<p>Advanced AI system for complex engineering problem solving</p>
|
||||
</div>
|
||||
<div className="chat-actions">
|
||||
<button
|
||||
className="action-btn secondary"
|
||||
onClick={createConversation}
|
||||
>
|
||||
🔄 New Conversation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Plans */}
|
||||
{pendingPlans.length > 0 && (
|
||||
<div className="pending-plans">
|
||||
<h3>📋 Pending Plans ({pendingPlans.length})</h3>
|
||||
<div className="plans-grid">
|
||||
{pendingPlans.map((plan) => (
|
||||
<div key={plan.id} className="plan-card">
|
||||
<div className="plan-header">
|
||||
<h4>{plan.title}</h4>
|
||||
<span className="plan-status">{plan.status}</span>
|
||||
</div>
|
||||
<div className="plan-content">
|
||||
<pre>{plan.content}</pre>
|
||||
</div>
|
||||
<div className="plan-actions">
|
||||
<button
|
||||
className="btn approve"
|
||||
onClick={() => approvePlan(plan.id)}
|
||||
>
|
||||
✅ Approve
|
||||
</button>
|
||||
<button
|
||||
className="btn reject"
|
||||
onClick={() => rejectPlan(plan.id)}
|
||||
>
|
||||
❌ Reject
|
||||
</button>
|
||||
<button
|
||||
className="btn execute"
|
||||
onClick={() => executePlan(plan.id)}
|
||||
>
|
||||
🚀 Execute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="messages-container">
|
||||
<div className="messages">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index} className={`message ${message.role}`} data-message-type={message.message_type}>
|
||||
<div className="message-avatar">
|
||||
{message.role === 'user' ? '👤' : '🤖'}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-header">
|
||||
<span className="message-role">
|
||||
{message.role === 'user' ? 'You' : 'Engineering AI'}
|
||||
</span>
|
||||
<span className="message-time">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
{message.role === 'assistant' && (
|
||||
<button
|
||||
className="feedback-btn"
|
||||
onClick={() => setShowFeedback(message.id || index)}
|
||||
>
|
||||
💬 Feedback
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="message-text">
|
||||
<pre>{message.content}</pre>
|
||||
</div>
|
||||
|
||||
{/* Plan Action Buttons */}
|
||||
{message.message_type === 'system_prompt' && message.content.includes('Engineering Plan Generated') && message.plan_id && (
|
||||
<div className="plan-actions">
|
||||
<button
|
||||
className="action-btn approve-btn"
|
||||
onClick={() => approvePlan(message.plan_id)}
|
||||
disabled={loading}
|
||||
>
|
||||
✅ Approve Plan
|
||||
</button>
|
||||
<button
|
||||
className="action-btn reject-btn"
|
||||
onClick={() => rejectPlan(message.plan_id)}
|
||||
disabled={loading}
|
||||
>
|
||||
❌ Reject Plan
|
||||
</button>
|
||||
<button
|
||||
className="action-btn modify-btn"
|
||||
onClick={() => setInputMessage('Please modify the plan with the following changes: ')}
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 Modify Plan
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && (
|
||||
<div className="message assistant">
|
||||
<div className="message-avatar">🤖</div>
|
||||
<div className="message-content">
|
||||
<div className="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="chat-input">
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask me about engineering problems, request analysis, or describe what you need help with..."
|
||||
rows="3"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
className="send-btn"
|
||||
onClick={sendMessage}
|
||||
disabled={!inputMessage.trim() || loading}
|
||||
>
|
||||
{loading ? '⏳' : '📤'} Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Modal */}
|
||||
{showFeedback && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>💬 Provide Feedback</h3>
|
||||
<button
|
||||
className="close-btn"
|
||||
onClick={() => setShowFeedback(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-content">
|
||||
<div className="form-group">
|
||||
<label>Feedback Type</label>
|
||||
<select
|
||||
value={feedbackData.feedbackType}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, feedbackType: e.target.value})}
|
||||
>
|
||||
<option value="positive">👍 Positive</option>
|
||||
<option value="negative">👎 Negative</option>
|
||||
<option value="correction">✏️ Correction</option>
|
||||
<option value="suggestion">💡 Suggestion</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Rating (1-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
value={feedbackData.rating}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, rating: parseInt(e.target.value)})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Comment</label>
|
||||
<textarea
|
||||
value={feedbackData.comment}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, comment: e.target.value})}
|
||||
placeholder="Share your thoughts about this response..."
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{feedbackData.feedbackType === 'correction' && (
|
||||
<div className="form-group">
|
||||
<label>Corrected Content</label>
|
||||
<textarea
|
||||
value={feedbackData.correctedContent}
|
||||
onChange={(e) => setFeedbackData({...feedbackData, correctedContent: e.target.value})}
|
||||
placeholder="Provide the corrected version..."
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="btn secondary"
|
||||
onClick={() => setShowFeedback(null)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn primary"
|
||||
onClick={() => submitFeedback(showFeedback)}
|
||||
>
|
||||
Submit Feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPage;
|
||||
@@ -0,0 +1,412 @@
|
||||
/* Dashboard Styles */
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.card-header p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Quick Actions */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-btn.blue:hover {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.action-btn.green:hover {
|
||||
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.action-btn.purple:hover {
|
||||
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.action-btn.orange:hover {
|
||||
background: linear-gradient(135deg, #fed7aa, #fdba74);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* System Status */
|
||||
.system-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-name {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.status-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Recent Activity */
|
||||
.recent-activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* System Overview */
|
||||
.system-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.overview-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import './Dashboard.css';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
conversations: 0,
|
||||
documents: 0,
|
||||
plans: 0,
|
||||
feedback: 0
|
||||
});
|
||||
const [recentActivity, setRecentActivity] = useState([]);
|
||||
const [systemStatus, setSystemStatus] = useState({
|
||||
model1: 'online',
|
||||
queryModel: 'online',
|
||||
rag: 'online'
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load conversations
|
||||
const conversationsRes = await axios.get('/api/chat/conversations?limit=5');
|
||||
|
||||
// Load documents
|
||||
const documentsRes = await axios.get('/api/documents?limit=5');
|
||||
|
||||
// Load system status
|
||||
const statusRes = await axios.get('/api/models/status');
|
||||
|
||||
setStats({
|
||||
conversations: conversationsRes.data.data.pagination.total,
|
||||
documents: documentsRes.data.data.pagination?.total || documentsRes.data.data.documents.length,
|
||||
plans: 0, // Will be calculated from conversations
|
||||
feedback: 0 // Will be loaded separately
|
||||
});
|
||||
|
||||
setRecentActivity(conversationsRes.data.data.conversations.slice(0, 5));
|
||||
setSystemStatus(statusRes.data.data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
title: 'Start Engineering Chat',
|
||||
description: 'Begin a new engineering conversation',
|
||||
icon: '💬',
|
||||
color: 'blue',
|
||||
action: () => navigate('/chat')
|
||||
},
|
||||
{
|
||||
title: 'Upload Documents',
|
||||
description: 'Add knowledge to the system',
|
||||
icon: '📚',
|
||||
color: 'green',
|
||||
action: () => navigate('/documents')
|
||||
},
|
||||
{
|
||||
title: 'Execute Tools',
|
||||
description: 'Run engineering tools and workflows',
|
||||
icon: '🔧',
|
||||
color: 'purple',
|
||||
action: () => navigate('/tools')
|
||||
},
|
||||
{
|
||||
title: 'View History',
|
||||
description: 'Browse past conversations',
|
||||
icon: '📖',
|
||||
color: 'orange',
|
||||
action: () => navigate('/chat')
|
||||
}
|
||||
];
|
||||
|
||||
const systemComponents = [
|
||||
{ name: 'MODEL1 (Planner)', status: systemStatus.model1, description: 'Engineering plan generation' },
|
||||
{ name: 'QUERYMODEL (Executor)', status: systemStatus.queryModel, description: 'Plan execution and tool orchestration' },
|
||||
{ name: 'RAG System', status: systemStatus.rag, description: 'Document search and knowledge retrieval' }
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<div className="dashboard-header">
|
||||
<h1>Engineering Reasoning Dashboard</h1>
|
||||
<p>Advanced AI system for complex engineering problem solving</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💬</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.conversations}</div>
|
||||
<div className="stat-label">Conversations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📚</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.documents}</div>
|
||||
<div className="stat-label">Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📋</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.plans}</div>
|
||||
<div className="stat-label">Plans Generated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">⭐</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">{stats.feedback}</div>
|
||||
<div className="stat-label">Feedback Items</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="dashboard-grid">
|
||||
{/* Quick Actions */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>Quick Actions</h2>
|
||||
<p>Start working with the system</p>
|
||||
</div>
|
||||
<div className="quick-actions">
|
||||
{quickActions.map((action, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`action-btn ${action.color}`}
|
||||
onClick={action.action}
|
||||
>
|
||||
<span className="action-icon">{action.icon}</span>
|
||||
<div className="action-content">
|
||||
<div className="action-title">{action.title}</div>
|
||||
<div className="action-description">{action.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>System Status</h2>
|
||||
<p>Current system health</p>
|
||||
</div>
|
||||
<div className="system-status">
|
||||
{systemComponents.map((component, index) => (
|
||||
<div key={index} className="status-item">
|
||||
<div className="status-info">
|
||||
<div className="status-name">{component.name}</div>
|
||||
<div className="status-description">{component.description}</div>
|
||||
</div>
|
||||
<div className={`status-indicator ${component.status}`}>
|
||||
<span className="status-dot"></span>
|
||||
{component.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>Recent Activity</h2>
|
||||
<p>Latest conversations and interactions</p>
|
||||
</div>
|
||||
<div className="recent-activity">
|
||||
{recentActivity.length > 0 ? (
|
||||
recentActivity.map((activity, index) => (
|
||||
<div key={index} className="activity-item">
|
||||
<div className="activity-icon">💬</div>
|
||||
<div className="activity-content">
|
||||
<div className="activity-title">{activity.title || 'Untitled Conversation'}</div>
|
||||
<div className="activity-meta">
|
||||
{new Date(activity.created_at).toLocaleDateString()} • {activity.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📝</div>
|
||||
<p>No recent activity</p>
|
||||
<button
|
||||
className="start-btn"
|
||||
onClick={() => navigate('/chat')}
|
||||
>
|
||||
Start Your First Conversation
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Overview */}
|
||||
<div className="dashboard-card">
|
||||
<div className="card-header">
|
||||
<h2>System Overview</h2>
|
||||
<p>How the engineering reasoning system works</p>
|
||||
</div>
|
||||
<div className="system-overview">
|
||||
<div className="overview-step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">MODEL1 Planning</div>
|
||||
<div className="step-description">Analyzes your engineering query and creates a structured plan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">Plan Approval</div>
|
||||
<div className="step-description">Review and approve the generated plan before execution</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">QUERYMODEL Execution</div>
|
||||
<div className="step-description">Executes the plan using specialized engineering tools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-step">
|
||||
<div className="step-number">4</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">Results & Feedback</div>
|
||||
<div className="step-description">Review results and provide feedback for continuous improvement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,538 @@
|
||||
/* Documents Page Styles */
|
||||
.documents-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.upload-section,
|
||||
.search-section,
|
||||
.documents-section {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Upload Section */
|
||||
.upload-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.file-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 1rem;
|
||||
background: #f9fafb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-input-label:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-info strong {
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-info span {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 150px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.search-type-selector {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: #f1f5f9;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.search-type-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.search-type-btn.active {
|
||||
background: white;
|
||||
color: #3b82f6;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.search-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Search Results */
|
||||
.search-results {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search-results h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.result-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.relevance-score {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-snippet {
|
||||
color: #475569;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.75rem;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Documents Grid */
|
||||
.documents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.document-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.document-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.document-header h4 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.document-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background: #fecaca;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.document-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status.processed {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.processing {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
color: #475569;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.search-input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-type-selector {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.documents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.upload-section,
|
||||
.search-section,
|
||||
.documents-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './DocumentsPage.css';
|
||||
|
||||
const DocumentsPage = () => {
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [searchType, setSearchType] = useState('semantic');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [uploadFile, setUploadFile] = useState(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadDocuments();
|
||||
}, []);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/documents');
|
||||
setDocuments(response.data.data.documents || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async () => {
|
||||
if (!uploadFile) return;
|
||||
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
setMessage('');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('document', uploadFile);
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/documents/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
setUploadProgress(percentCompleted);
|
||||
}
|
||||
});
|
||||
|
||||
setMessage(`✅ Document uploaded successfully: ${response.data.data.document.original_filename}`);
|
||||
setUploadFile(null);
|
||||
setUploadProgress(0);
|
||||
loadDocuments();
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
setMessage(`❌ Upload failed: ${error.response?.data?.error || error.message}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
setSearching(true);
|
||||
setSearchResults([]);
|
||||
|
||||
try {
|
||||
const endpoint = searchType === 'semantic' ? '/api/documents/search' : '/api/documents/graph-search';
|
||||
const response = await axios.get(endpoint, {
|
||||
params: { query: searchQuery }
|
||||
});
|
||||
|
||||
setSearchResults(response.data.data.results || []);
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
setMessage(`❌ Search failed: ${error.response?.data?.error || error.message}`);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDocument = async (documentId) => {
|
||||
if (!window.confirm('Are you sure you want to delete this document?')) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/documents/${documentId}`);
|
||||
setMessage('✅ Document deleted successfully');
|
||||
loadDocuments();
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
setMessage(`❌ Delete failed: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="documents-page">
|
||||
<div className="page-header">
|
||||
<h1>📚 Knowledge Base</h1>
|
||||
<p>Upload and search engineering documents with advanced AI</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Section */}
|
||||
<div className="upload-section">
|
||||
<div className="section-header">
|
||||
<h2>📤 Upload Documents</h2>
|
||||
<p>Add new documents to the knowledge base</p>
|
||||
</div>
|
||||
|
||||
<div className="upload-area">
|
||||
<div className="file-input-container">
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
accept=".pdf,.txt,.doc,.docx"
|
||||
onChange={(e) => setUploadFile(e.target.files?.[0])}
|
||||
className="file-input"
|
||||
/>
|
||||
<label htmlFor="file-upload" className="file-input-label">
|
||||
<div className="upload-icon">📁</div>
|
||||
<div className="upload-text">
|
||||
{uploadFile ? uploadFile.name : 'Choose a file to upload'}
|
||||
</div>
|
||||
<div className="upload-hint">PDF, TXT, DOC, DOCX files supported</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{uploadFile && (
|
||||
<div className="upload-actions">
|
||||
<div className="file-info">
|
||||
<strong>{uploadFile.name}</strong>
|
||||
<span>{formatFileSize(uploadFile.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
className="upload-btn"
|
||||
onClick={handleFileUpload}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span>Uploading... {uploadProgress}%</span>
|
||||
</>
|
||||
) : (
|
||||
'📤 Upload Document'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="search-section">
|
||||
<div className="section-header">
|
||||
<h2>🔍 Document Search</h2>
|
||||
<p>Find relevant information using AI-powered search</p>
|
||||
</div>
|
||||
|
||||
<div className="search-container">
|
||||
<div className="search-input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search for engineering concepts, specifications, or procedures..."
|
||||
className="search-input"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<div className="search-type-selector">
|
||||
<button
|
||||
className={`search-type-btn ${searchType === 'semantic' ? 'active' : ''}`}
|
||||
onClick={() => setSearchType('semantic')}
|
||||
>
|
||||
🔍 Semantic
|
||||
</button>
|
||||
<button
|
||||
className={`search-type-btn ${searchType === 'graph' ? 'active' : ''}`}
|
||||
onClick={() => setSearchType('graph')}
|
||||
>
|
||||
🕸️ Graph RAG
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="search-btn"
|
||||
onClick={handleSearch}
|
||||
disabled={!searchQuery.trim() || searching}
|
||||
>
|
||||
{searching ? '⏳' : '🔍'} Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<div className="search-results">
|
||||
<h3>Search Results ({searchResults.length})</h3>
|
||||
<div className="results-grid">
|
||||
{searchResults.map((result, index) => (
|
||||
<div key={index} className="result-card">
|
||||
<div className="result-header">
|
||||
<h4>📄 {result.original_filename}</h4>
|
||||
<span className="relevance-score">
|
||||
{result.score?.toFixed(3)} relevance
|
||||
</span>
|
||||
</div>
|
||||
<div className="result-snippet">
|
||||
{result.snippet}
|
||||
</div>
|
||||
<div className="result-meta">
|
||||
<span>ID: {result.id}</span>
|
||||
<span>Size: {formatFileSize(result.file_size || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents List */}
|
||||
<div className="documents-section">
|
||||
<div className="section-header">
|
||||
<h2>📋 Document Library</h2>
|
||||
<p>Manage your uploaded documents</p>
|
||||
</div>
|
||||
|
||||
<div className="documents-grid">
|
||||
{documents.length > 0 ? (
|
||||
documents.map((doc) => (
|
||||
<div key={doc.id} className="document-card">
|
||||
<div className="document-header">
|
||||
<h4>📄 {doc.original_filename}</h4>
|
||||
<div className="document-actions">
|
||||
<button
|
||||
className="action-btn delete"
|
||||
onClick={() => deleteDocument(doc.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-meta">
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Size:</span>
|
||||
<span>{formatFileSize(doc.file_size)}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Uploaded:</span>
|
||||
<span>{new Date(doc.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Status:</span>
|
||||
<span className={`status ${doc.status}`}>{doc.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
{doc.extracted_text && (
|
||||
<div className="document-preview">
|
||||
<div className="preview-header">Content Preview:</div>
|
||||
<div className="preview-text">
|
||||
{doc.extracted_text.substring(0, 200)}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📚</div>
|
||||
<h3>No documents yet</h3>
|
||||
<p>Upload your first document to start building your knowledge base</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentsPage;
|
||||
@@ -0,0 +1,226 @@
|
||||
/* Login Page Styles */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-tabs {
|
||||
display: flex;
|
||||
background: #f1f5f9;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: white;
|
||||
color: #3b82f6;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.switch-btn:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-pattern {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background:
|
||||
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(180deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './LoginPage.css';
|
||||
|
||||
const LoginPage = () => {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
firstName: '',
|
||||
lastName: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login, register, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const result = isLogin
|
||||
? await login(formData.email, formData.password)
|
||||
: await register(formData);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-container">
|
||||
<div className="login-header">
|
||||
<div className="logo">
|
||||
<span className="logo-icon">🧠</span>
|
||||
<span className="logo-text">Reason Flow</span>
|
||||
</div>
|
||||
<p className="login-subtitle">
|
||||
Advanced Engineering Reasoning System
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-form-container">
|
||||
<div className="form-tabs">
|
||||
<button
|
||||
className={`tab ${isLogin ? 'active' : ''}`}
|
||||
onClick={() => setIsLogin(true)}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${!isLogin ? 'active' : ''}`}
|
||||
onClick={() => setIsLogin(false)}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
{!isLogin && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
placeholder="Enter your first name"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
placeholder="Enter your last name"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
minLength="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-btn"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="loading-spinner"></span>
|
||||
) : (
|
||||
isLogin ? 'Sign In' : 'Create Account'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>
|
||||
{isLogin ? "Don't have an account?" : "Already have an account?"}
|
||||
<button
|
||||
type="button"
|
||||
className="switch-btn"
|
||||
onClick={() => setIsLogin(!isLogin)}
|
||||
>
|
||||
{isLogin ? 'Register' : 'Login'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-background">
|
||||
<div className="bg-pattern"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,525 @@
|
||||
/* Tools Page Styles */
|
||||
.tools-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #1e293b, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fde68a;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.tool-selection,
|
||||
.execution-history {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tool Grid */
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-card.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #f0f9ff;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.tool-card.blue.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.tool-card.green.selected {
|
||||
border-color: #10b981;
|
||||
background: #d1fae5;
|
||||
}
|
||||
|
||||
.tool-card.purple.selected {
|
||||
border-color: #8b5cf6;
|
||||
background: #e9d5ff;
|
||||
}
|
||||
|
||||
.tool-card.orange.selected {
|
||||
border-color: #f59e0b;
|
||||
background: #fed7aa;
|
||||
}
|
||||
|
||||
.tool-card.cyan.selected {
|
||||
border-color: #06b6d4;
|
||||
background: #cffafe;
|
||||
}
|
||||
|
||||
.tool-card.red.selected {
|
||||
border-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tool-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-info p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Execution Panel */
|
||||
.execution-panel {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.input-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.execution-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.execute-btn {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.execute-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.execute-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.execute-btn.disabled {
|
||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.execute-btn.disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Execution History */
|
||||
.history-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.executions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.execution-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.execution-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.execution-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.execution-info h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.execution-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.execution-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-badge.info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Execution Details */
|
||||
.execution-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-section.error {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.detail-section h5 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.code-block,
|
||||
.error-block {
|
||||
background: #f1f5f9;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.error-block {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.execution-metrics {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.tool-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.execution-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.execution-status {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.execution-metrics {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.tool-selection,
|
||||
.execution-history {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import './ToolsPage.css';
|
||||
|
||||
const ToolsPage = () => {
|
||||
const [toolExecutions, setToolExecutions] = useState([]);
|
||||
const [selectedTool, setSelectedTool] = useState('');
|
||||
const [toolInput, setToolInput] = useState('');
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const tools = [
|
||||
{
|
||||
id: 'query_expander',
|
||||
name: 'Query Expander',
|
||||
description: 'Expands and refines engineering queries for better understanding',
|
||||
icon: '🔍',
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
id: 'extraction',
|
||||
name: 'Document Extraction',
|
||||
description: 'Extracts relevant information from uploaded documents',
|
||||
icon: '📄',
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
id: 'report1',
|
||||
name: 'Report Generator',
|
||||
description: 'Generates structured engineering reports',
|
||||
icon: '📊',
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
id: 'report2',
|
||||
name: 'File Generator',
|
||||
description: 'Creates downloadable engineering files',
|
||||
icon: '📁',
|
||||
color: 'orange'
|
||||
},
|
||||
{
|
||||
id: 'web_search',
|
||||
name: 'Web Search',
|
||||
description: 'Searches the web for current engineering information',
|
||||
icon: '🌐',
|
||||
color: 'cyan'
|
||||
},
|
||||
{
|
||||
id: 'encyclopedia_pdf',
|
||||
name: 'Encyclopedia Search',
|
||||
description: 'Searches internal PDF knowledge base',
|
||||
icon: '📚',
|
||||
color: 'red'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadToolExecutions();
|
||||
}, []);
|
||||
|
||||
const loadToolExecutions = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/tools/executions');
|
||||
setToolExecutions(response.data.data.executions || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading tool executions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const executeTool = async () => {
|
||||
setMessage('⚠️ **Manual tool execution is disabled.** Tools can only be executed as part of an approved engineering plan. Please use the Chat page to create and execute plans.');
|
||||
setSelectedTool('');
|
||||
setToolInput('');
|
||||
};
|
||||
|
||||
const retryExecution = async (executionId) => {
|
||||
try {
|
||||
await axios.post(`/api/tools/executions/${executionId}/retry`);
|
||||
setMessage('✅ Tool execution retried');
|
||||
loadToolExecutions();
|
||||
} catch (error) {
|
||||
console.error('Retry error:', error);
|
||||
setMessage(`❌ Retry failed: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'success';
|
||||
case 'failed': return 'error';
|
||||
case 'running': return 'warning';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tools-page">
|
||||
<div className="page-header">
|
||||
<h1>🔧 Engineering Tools</h1>
|
||||
<p>View tool execution history and results from approved engineering plans</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('✅') ? 'success' : message.includes('⚠️') ? 'warning' : 'error'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Selection */}
|
||||
<div className="tool-selection">
|
||||
<div className="section-header">
|
||||
<h2>🛠️ Tool Information</h2>
|
||||
<p>Available engineering tools (execution requires approved plan)</p>
|
||||
</div>
|
||||
|
||||
<div className="tool-selector">
|
||||
<div className="tool-grid">
|
||||
{tools.map((tool) => (
|
||||
<button
|
||||
key={tool.id}
|
||||
className={`tool-card ${selectedTool === tool.id ? 'selected' : ''} ${tool.color}`}
|
||||
onClick={() => setSelectedTool(tool.id)}
|
||||
>
|
||||
<div className="tool-icon">{tool.icon}</div>
|
||||
<div className="tool-info">
|
||||
<h3>{tool.name}</h3>
|
||||
<p>{tool.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTool && (
|
||||
<div className="execution-panel">
|
||||
<div className="panel-header">
|
||||
<h3>{tools.find(t => t.id === selectedTool)?.name} Information</h3>
|
||||
</div>
|
||||
<div className="panel-content">
|
||||
<div className="tool-info">
|
||||
<p><strong>Description:</strong> {tools.find(t => t.id === selectedTool)?.description}</p>
|
||||
<p><strong>Usage:</strong> This tool is automatically executed as part of approved engineering plans.</p>
|
||||
<p><strong>To use this tool:</strong></p>
|
||||
<ol>
|
||||
<li>Go to the Chat page</li>
|
||||
<li>Ask an engineering question</li>
|
||||
<li>Review the generated plan</li>
|
||||
<li>Approve the plan</li>
|
||||
<li>The system will automatically execute the appropriate tools</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="execution-actions">
|
||||
<button
|
||||
className="execute-btn disabled"
|
||||
onClick={executeTool}
|
||||
disabled={true}
|
||||
>
|
||||
🔒 Manual Execution Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Execution History */}
|
||||
<div className="execution-history">
|
||||
<div className="section-header">
|
||||
<h2>📋 Execution History</h2>
|
||||
<p>View past tool executions and their results</p>
|
||||
</div>
|
||||
|
||||
<div className="history-container">
|
||||
{toolExecutions.length > 0 ? (
|
||||
<div className="executions-list">
|
||||
{toolExecutions.map((execution) => (
|
||||
<div key={execution.id} className="execution-card">
|
||||
<div className="execution-header">
|
||||
<div className="execution-info">
|
||||
<h4>
|
||||
{tools.find(t => t.id === execution.tool_name)?.icon}
|
||||
{tools.find(t => t.id === execution.tool_name)?.name || execution.tool_name}
|
||||
</h4>
|
||||
<div className="execution-meta">
|
||||
<span className="execution-id">ID: {execution.id}</span>
|
||||
<span className="execution-time">{formatDate(execution.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="execution-status">
|
||||
<span className={`status-badge ${getStatusColor(execution.status)}`}>
|
||||
{execution.status}
|
||||
</span>
|
||||
{execution.status === 'failed' && (
|
||||
<button
|
||||
className="retry-btn"
|
||||
onClick={() => retryExecution(execution.id)}
|
||||
>
|
||||
🔄 Retry
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="execution-details">
|
||||
<div className="detail-section">
|
||||
<h5>Input Parameters</h5>
|
||||
<pre className="code-block">
|
||||
{JSON.stringify(execution.input_parameters, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{execution.output_result && (
|
||||
<div className="detail-section">
|
||||
<h5>Output Result</h5>
|
||||
<pre className="code-block">
|
||||
{JSON.stringify(execution.output_result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{execution.error_message && (
|
||||
<div className="detail-section error">
|
||||
<h5>Error Message</h5>
|
||||
<pre className="error-block">
|
||||
{execution.error_message}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="execution-metrics">
|
||||
<div className="metric">
|
||||
<span className="metric-label">Duration:</span>
|
||||
<span className="metric-value">{execution.duration || 'N/A'}ms</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="metric-label">Tokens Used:</span>
|
||||
<span className="metric-value">{execution.tokens_used || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">🔧</div>
|
||||
<h3>No tool executions yet</h3>
|
||||
<p>Execute your first tool to see the results here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsPage;
|
||||
Reference in New Issue
Block a user