first commit

This commit is contained in:
2025-11-06 11:08:59 +01:00
commit 3c5117c2c3
85 changed files with 13275 additions and 0 deletions
+51
View File
@@ -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"
}
+20
View File
@@ -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>
+15
View File
@@ -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"
}
+82
View File
@@ -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;
+283
View File
@@ -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;
}
}
+114
View File
@@ -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;
+94
View File
@@ -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>
);
};
+80
View File
@@ -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); }
}
+14
View File
@@ -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>
);
+515
View File
@@ -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;
}
}
+338
View File
@@ -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;
+631
View File
@@ -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;
}
}
+571
View File
@@ -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;
+412
View File
@@ -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;
}
}
+267
View File
@@ -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;
+538
View File
@@ -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;
}
}
+300
View File
@@ -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;
+226
View File
@@ -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;
}
}
+182
View File
@@ -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;
+525
View File
@@ -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;
}
}
+261
View File
@@ -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;