WIP: Save current work before CHORUS rebrand
- Agent roles integration progress - Various backend and frontend updates - Storybook cache cleanup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { ReactFlowProvider } from 'reactflow'
|
||||
import Layout from './components/Layout'
|
||||
import { SocketIOProvider } from './contexts/SocketIOContext'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import ProtectedRoute from './components/auth/ProtectedRoute'
|
||||
import Login from './pages/Login'
|
||||
import UserProfile from './components/auth/UserProfile'
|
||||
@@ -19,10 +20,11 @@ import ProjectForm from './components/projects/ProjectForm'
|
||||
import WorkflowEditor from './components/workflows/WorkflowEditor'
|
||||
import WorkflowDashboard from './components/workflows/WorkflowDashboard'
|
||||
import ClusterNodes from './components/cluster/ClusterNodes'
|
||||
import BzzzChat from './pages/BzzzChat'
|
||||
|
||||
function App() {
|
||||
// Check for connection issues and provide fallback
|
||||
const socketIOEnabled = import.meta.env.VITE_DISABLE_SOCKETIO !== 'true';
|
||||
const socketIOEnabled = false; // Temporarily disable to prevent connection loops
|
||||
|
||||
const AppContent = () => (
|
||||
<Routes>
|
||||
@@ -130,6 +132,15 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Bzzz Chat */}
|
||||
<Route path="/bzzz-chat" element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<BzzzChat />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Executions */}
|
||||
<Route path="/executions" element={
|
||||
<ProtectedRoute>
|
||||
@@ -173,17 +184,19 @@ function App() {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<AuthProvider>
|
||||
<ReactFlowProvider>
|
||||
{socketIOEnabled ? (
|
||||
<SocketIOProvider>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ReactFlowProvider>
|
||||
{socketIOEnabled ? (
|
||||
<SocketIOProvider>
|
||||
<AppContent />
|
||||
</SocketIOProvider>
|
||||
) : (
|
||||
<AppContent />
|
||||
</SocketIOProvider>
|
||||
) : (
|
||||
<AppContent />
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
</AuthProvider>
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
// Types
|
||||
export interface Agent {
|
||||
@@ -97,7 +98,8 @@ export interface AgentHealth {
|
||||
|
||||
// API client
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VITE_API_BASE_URL || 'http://localhost:8087',
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
// Types
|
||||
export interface LoginRequest {
|
||||
@@ -55,7 +56,8 @@ export interface CreateAPIKeyResponse {
|
||||
|
||||
// API client
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VITE_API_BASE_URL || 'http://localhost:8087',
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
// Types
|
||||
export interface SystemHealth {
|
||||
@@ -98,7 +99,8 @@ export interface AlertRule {
|
||||
|
||||
// API client
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VITE_API_BASE_URL || 'http://localhost:8087',
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
// Types
|
||||
export interface Task {
|
||||
@@ -49,7 +50,8 @@ export interface TaskStatistics {
|
||||
|
||||
// API client
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VITE_API_BASE_URL || 'http://localhost:8087',
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
BIN
frontend/src/assets/Hive_symbol.png
Normal file
BIN
frontend/src/assets/Hive_symbol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -12,10 +12,13 @@ import {
|
||||
ComputerDesktopIcon,
|
||||
UserCircleIcon,
|
||||
ChevronDownIcon,
|
||||
AdjustmentsHorizontalIcon
|
||||
AdjustmentsHorizontalIcon,
|
||||
ChatBubbleLeftRightIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import UserProfile from './auth/UserProfile';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import HiveLogo from '../assets/Hive_symbol.png';
|
||||
|
||||
interface NavigationItem {
|
||||
name: string;
|
||||
@@ -31,6 +34,7 @@ const navigation: NavigationItem[] = [
|
||||
{ name: 'Cluster', href: '/cluster', icon: ComputerDesktopIcon },
|
||||
{ name: 'Executions', href: '/executions', icon: PlayIcon },
|
||||
{ name: 'Agents', href: '/agents', icon: UserGroupIcon },
|
||||
{ name: 'Bzzz Chat', href: '/bzzz-chat', icon: ChatBubbleLeftRightIcon },
|
||||
{ name: 'Analytics', href: '/analytics', icon: ChartBarIcon },
|
||||
{ name: 'Settings', href: '/settings', icon: AdjustmentsHorizontalIcon },
|
||||
];
|
||||
@@ -67,7 +71,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
|
||||
{/* Mobile sidebar overlay */}
|
||||
{sidebarOpen && (
|
||||
<div className="fixed inset-0 z-40 lg:hidden">
|
||||
@@ -75,15 +79,15 @@ export default function Layout({ children }: LayoutProps) {
|
||||
className="fixed inset-0 bg-gray-600 bg-opacity-75"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
<div className="fixed inset-y-0 left-0 flex flex-col w-64 bg-white shadow-xl">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="fixed inset-y-0 left-0 flex flex-col w-64 bg-white dark:bg-gray-800 shadow-xl">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-2xl">🐝</span>
|
||||
<span className="text-lg font-semibold text-gray-900">Hive</span>
|
||||
<img src={HiveLogo} alt="Hive" className="h-8 w-8 object-contain" />
|
||||
<span className="text-lg font-semibold text-gray-900 dark:text-white">Hive</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
className="text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
@@ -96,13 +100,13 @@ export default function Layout({ children }: LayoutProps) {
|
||||
className={`
|
||||
group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors
|
||||
${item.current
|
||||
? 'bg-blue-100 text-blue-900'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white'
|
||||
}
|
||||
`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${item.current ? 'text-blue-500' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${item.current ? 'text-blue-500' : 'text-gray-400 dark:text-gray-500'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
@@ -113,10 +117,10 @@ export default function Layout({ children }: LayoutProps) {
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="flex flex-col w-64 bg-white border-r border-gray-200">
|
||||
<div className="flex items-center px-6 py-4 border-b">
|
||||
<span className="text-2xl mr-2">🐝</span>
|
||||
<span className="text-xl font-semibold text-gray-900">Hive</span>
|
||||
<div className="flex flex-col w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<img src={HiveLogo} alt="Hive" className="h-8 w-8 object-contain mr-2" />
|
||||
<span className="text-xl font-semibold text-gray-900 dark:text-white">Hive</span>
|
||||
</div>
|
||||
<nav className="flex-1 px-4 py-4 space-y-1">
|
||||
{navigationWithCurrent.map((item) => (
|
||||
@@ -126,20 +130,20 @@ export default function Layout({ children }: LayoutProps) {
|
||||
className={`
|
||||
group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors
|
||||
${item.current
|
||||
? 'bg-blue-100 text-blue-900'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100'
|
||||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${item.current ? 'text-blue-500' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${item.current ? 'text-blue-500' : 'text-gray-400 dark:text-gray-500'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="border-t p-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||
<span>All systems operational</span>
|
||||
</div>
|
||||
@@ -150,41 +154,44 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-gray-200 px-4 py-2">
|
||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="lg:hidden text-gray-400 hover:text-gray-600"
|
||||
className="lg:hidden text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
<Bars3Icon className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="lg:hidden flex items-center space-x-2">
|
||||
<span className="text-2xl">🐝</span>
|
||||
<span className="text-lg font-semibold text-gray-900">Hive</span>
|
||||
<span className="text-lg font-semibold text-gray-900 dark:text-white">Hive</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User menu */}
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className="flex items-center space-x-2 text-sm text-gray-700 hover:text-gray-900 focus:outline-none"
|
||||
>
|
||||
<UserCircleIcon className="h-8 w-8 text-gray-400" />
|
||||
<span className="hidden sm:block">{user?.name || user?.full_name || user?.username}</span>
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
{/* Theme toggle and User menu */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<ThemeToggle />
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className="flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white focus:outline-none"
|
||||
>
|
||||
<UserCircleIcon className="h-8 w-8 text-gray-400 dark:text-gray-500" />
|
||||
<span className="hidden sm:block">{user?.name || user?.full_name || user?.username}</span>
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* User dropdown */}
|
||||
{userMenuOpen && (
|
||||
<div className="absolute right-0 mt-2 z-50">
|
||||
<UserProfile
|
||||
isDropdown={true}
|
||||
onClose={() => setUserMenuOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* User dropdown */}
|
||||
{userMenuOpen && (
|
||||
<div className="absolute right-0 mt-2 z-50">
|
||||
<UserProfile
|
||||
isDropdown={true}
|
||||
onClose={() => setUserMenuOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
32
frontend/src/components/ThemeToggle.tsx
Normal file
32
frontend/src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className={`
|
||||
inline-flex items-center justify-center p-2 rounded-md
|
||||
text-gray-600 hover:text-gray-900 hover:bg-gray-100
|
||||
dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700
|
||||
transition-colors duration-200
|
||||
${className}
|
||||
`}
|
||||
aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<SunIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<MoonIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
95
frontend/src/config/api.ts
Normal file
95
frontend/src/config/api.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* API Configuration
|
||||
* Centralizes API endpoint configuration and environment handling
|
||||
*/
|
||||
|
||||
// Get base URL from environment with smart fallbacks
|
||||
export const getApiBaseUrl = (): string => {
|
||||
// Production environment
|
||||
if (import.meta.env.PROD) {
|
||||
return import.meta.env.VITE_API_BASE_URL || 'https://hive.home.deepblack.cloud';
|
||||
}
|
||||
|
||||
// Development environment - prefer environment variable
|
||||
const envUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
if (envUrl) {
|
||||
return envUrl;
|
||||
}
|
||||
|
||||
// Development fallback - detect if we're running in docker
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// If we're on the production domain, use production API
|
||||
if (hostname === 'hive.home.deepblack.cloud') {
|
||||
return 'https://hive.home.deepblack.cloud';
|
||||
}
|
||||
|
||||
// If we're on localhost, try to detect the backend port
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
// Check if backend is running on standard development port
|
||||
return 'http://localhost:8089'; // Dev backend port (avoiding filebrowser on 8088)
|
||||
}
|
||||
|
||||
// Docker container or other environments
|
||||
return 'http://localhost:8087'; // Production backend port
|
||||
};
|
||||
|
||||
// Get WebSocket URL from environment with smart fallbacks
|
||||
export const getWebSocketUrl = (): string => {
|
||||
// Production environment
|
||||
if (import.meta.env.PROD) {
|
||||
return import.meta.env.VITE_WS_BASE_URL || 'https://hive.home.deepblack.cloud';
|
||||
}
|
||||
|
||||
// Development environment
|
||||
const envUrl = import.meta.env.VITE_WS_BASE_URL;
|
||||
if (envUrl) {
|
||||
return envUrl;
|
||||
}
|
||||
|
||||
// Smart fallback based on current hostname
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
if (hostname === 'hive.home.deepblack.cloud') {
|
||||
return 'https://hive.home.deepblack.cloud';
|
||||
}
|
||||
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return 'http://localhost:8089'; // Dev backend port (avoiding filebrowser on 8088)
|
||||
}
|
||||
|
||||
return 'http://localhost:8087'; // Production backend port
|
||||
};
|
||||
|
||||
// Debug configuration
|
||||
export const isDebugMode = (): boolean => {
|
||||
return import.meta.env.VITE_ENABLE_DEBUG_MODE === 'true' || import.meta.env.DEV;
|
||||
};
|
||||
|
||||
// Development mode detection
|
||||
export const isDevMode = (): boolean => {
|
||||
return import.meta.env.VITE_DEV_MODE === 'true' || import.meta.env.DEV;
|
||||
};
|
||||
|
||||
// Log level configuration
|
||||
export const getLogLevel = (): string => {
|
||||
return import.meta.env.VITE_LOG_LEVEL || (import.meta.env.DEV ? 'debug' : 'warn');
|
||||
};
|
||||
|
||||
// API configuration object
|
||||
export const apiConfig = {
|
||||
baseURL: getApiBaseUrl(),
|
||||
websocketURL: getWebSocketUrl(),
|
||||
debug: isDebugMode(),
|
||||
dev: isDevMode(),
|
||||
logLevel: getLogLevel(),
|
||||
timeout: 30000, // 30 seconds
|
||||
retries: 3,
|
||||
};
|
||||
|
||||
// Log configuration in development
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔧 API Configuration:', apiConfig);
|
||||
}
|
||||
|
||||
export default apiConfig;
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
interface User {
|
||||
id: string; // UUID as string
|
||||
@@ -45,7 +46,13 @@ interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL + '/api' || '/api';
|
||||
const API_BASE_URL = apiConfig.baseURL + '/api';
|
||||
|
||||
// Debug logging
|
||||
if (apiConfig.debug) {
|
||||
console.log('Auth API_BASE_URL:', API_BASE_URL);
|
||||
console.log('apiConfig.baseURL:', apiConfig.baseURL);
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
@@ -127,6 +134,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
|
||||
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
|
||||
localStorage.setItem('hive_user', JSON.stringify(data.user));
|
||||
localStorage.setItem('token', newTokens.access_token);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
@@ -168,6 +176,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
// Store in localStorage
|
||||
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
|
||||
localStorage.setItem('hive_user', JSON.stringify(data.user));
|
||||
localStorage.setItem('token', newTokens.access_token);
|
||||
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message || 'Login failed');
|
||||
@@ -203,6 +212,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
// Store in localStorage
|
||||
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
|
||||
localStorage.setItem('hive_user', JSON.stringify(data.user));
|
||||
localStorage.setItem('token', newTokens.access_token);
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error);
|
||||
throw error;
|
||||
@@ -248,6 +258,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
setTokens(null);
|
||||
localStorage.removeItem('hive_tokens');
|
||||
localStorage.removeItem('hive_user');
|
||||
localStorage.removeItem('token');
|
||||
};
|
||||
|
||||
const contextValue: AuthContextType = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
|
||||
import { useSocketIO, SocketIOMessage } from '../hooks/useSocketIO';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
interface SocketIOContextType {
|
||||
isConnected: boolean;
|
||||
@@ -45,6 +46,8 @@ export const SocketIOProvider: React.FC<SocketIOProviderProps> = ({
|
||||
</SocketIOContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [subscriptions, setSubscriptions] = useState<Map<string, Set<(data: any) => void>>>(new Map());
|
||||
|
||||
const {
|
||||
@@ -91,6 +94,21 @@ export const SocketIOProvider: React.FC<SocketIOProviderProps> = ({
|
||||
// console.error('Socket.IO error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Reconnect when authentication status changes (but only once)
|
||||
const hasReconnectedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !hasReconnectedRef.current) {
|
||||
console.log('User authenticated, reconnecting Socket.IO...');
|
||||
hasReconnectedRef.current = true;
|
||||
setTimeout(() => {
|
||||
reconnect();
|
||||
}, 1000); // Delay to prevent immediate reconnection loop
|
||||
}
|
||||
if (!isAuthenticated) {
|
||||
hasReconnectedRef.current = false;
|
||||
}
|
||||
}, [isAuthenticated, reconnect]);
|
||||
|
||||
const subscribe = (messageType: string, handler: (data: any) => void) => {
|
||||
setSubscriptions(prev => {
|
||||
|
||||
65
frontend/src/contexts/ThemeContext.tsx
Normal file
65
frontend/src/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface ThemeContextType {
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
setDarkMode: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
// Check localStorage for saved preference
|
||||
const saved = localStorage.getItem('darkMode');
|
||||
if (saved !== null) {
|
||||
return JSON.parse(saved);
|
||||
}
|
||||
// Default to system preference
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Apply dark mode class to document
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Save preference to localStorage
|
||||
localStorage.setItem('darkMode', JSON.stringify(isDarkMode));
|
||||
}, [isDarkMode]);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode(!isDarkMode);
|
||||
};
|
||||
|
||||
const setDarkMode = (enabled: boolean) => {
|
||||
setIsDarkMode(enabled);
|
||||
};
|
||||
|
||||
const value = {
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
setDarkMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -61,6 +61,9 @@ export const useSocketIO = (options: SocketIOHookOptions): SocketIOHookReturn =>
|
||||
setConnectionState('connecting');
|
||||
console.log('Socket.IO connecting to:', url);
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
console.log('Socket.IO auth token available:', !!token);
|
||||
|
||||
const socketInstance = io(url, {
|
||||
transports: ['websocket', 'polling'],
|
||||
upgrade: true,
|
||||
@@ -71,7 +74,10 @@ export const useSocketIO = (options: SocketIOHookOptions): SocketIOHookReturn =>
|
||||
reconnectionDelay,
|
||||
timeout: 20000,
|
||||
forceNew: false,
|
||||
path: '/socket.io/'
|
||||
path: '/socket.io/',
|
||||
auth: {
|
||||
token: token ? `Bearer ${token}` : undefined
|
||||
}
|
||||
});
|
||||
|
||||
socketInstance.on('connect', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { apiConfig, getWebSocketUrl } from '../config/api';
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: string;
|
||||
@@ -54,9 +55,11 @@ export const useWebSocket = (options: WebSocketHookOptions): WebSocketHookReturn
|
||||
|
||||
try {
|
||||
setConnectionState('connecting');
|
||||
// Ensure we use the correct URL in production
|
||||
const wsUrl = url.includes('localhost') ? 'wss://hive.home.deepblack.cloud/socket.io/general' : url;
|
||||
console.log('WebSocket connecting to:', wsUrl);
|
||||
// Use smart URL detection from config
|
||||
const wsUrl = getWebSocketUrl().replace('http', 'ws') + '/socket.io/general';
|
||||
if (apiConfig.debug) {
|
||||
console.log('WebSocket connecting to:', wsUrl);
|
||||
}
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-family: 'Fira Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* Dark mode color overrides for root */
|
||||
:root.dark {
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
|
||||
@@ -68,125 +68,12 @@ export default function Agents() {
|
||||
agent_type: 'gemini'
|
||||
});
|
||||
|
||||
const { data: agents = [], isLoading, refetch } = useQuery({
|
||||
const { data: agents = [], isLoading, error: agentsError, refetch } = useQuery({
|
||||
queryKey: ['agents'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await agentApi.getAgents();
|
||||
} catch (err) {
|
||||
// Return mock data if API fails - mixed agent types
|
||||
return [
|
||||
{
|
||||
id: 'walnut-ollama',
|
||||
name: 'WALNUT',
|
||||
endpoint: 'http://192.168.1.27:11434',
|
||||
model: 'deepseek-coder-v2:latest',
|
||||
specialty: 'frontend',
|
||||
status: 'online',
|
||||
agent_type: 'ollama',
|
||||
max_concurrent: 2,
|
||||
current_tasks: 1,
|
||||
last_seen: new Date().toISOString(),
|
||||
capabilities: ['React', 'TypeScript', 'TailwindCSS'],
|
||||
metrics: {
|
||||
tasks_completed: 45,
|
||||
uptime: '23h 45m',
|
||||
response_time: 2.3
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ironwood-ollama',
|
||||
name: 'IRONWOOD',
|
||||
endpoint: 'http://192.168.1.113:11434',
|
||||
model: 'qwen2.5-coder:latest',
|
||||
specialty: 'backend',
|
||||
status: 'online',
|
||||
agent_type: 'ollama',
|
||||
max_concurrent: 2,
|
||||
current_tasks: 0,
|
||||
last_seen: new Date().toISOString(),
|
||||
capabilities: ['Python', 'FastAPI', 'PostgreSQL'],
|
||||
metrics: {
|
||||
tasks_completed: 32,
|
||||
uptime: '18h 12m',
|
||||
response_time: 1.8
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'acacia',
|
||||
name: 'ACACIA',
|
||||
endpoint: 'http://192.168.1.72:11434',
|
||||
model: 'qwen2.5:latest',
|
||||
specialty: 'documentation',
|
||||
status: 'offline',
|
||||
agent_type: 'ollama',
|
||||
max_concurrent: 1,
|
||||
current_tasks: 0,
|
||||
last_seen: new Date(Date.now() - 3600000).toISOString(),
|
||||
capabilities: ['Documentation', 'Testing', 'QA'],
|
||||
metrics: {
|
||||
tasks_completed: 18,
|
||||
uptime: '0h 0m',
|
||||
response_time: 0
|
||||
}
|
||||
},
|
||||
// CLI Agents
|
||||
{
|
||||
id: 'walnut-gemini',
|
||||
name: 'WALNUT-GEMINI',
|
||||
endpoint: 'cli://walnut',
|
||||
model: 'gemini-2.5-pro',
|
||||
specialty: 'general_ai',
|
||||
status: 'available',
|
||||
agent_type: 'cli',
|
||||
max_concurrent: 2,
|
||||
current_tasks: 0,
|
||||
last_seen: new Date().toISOString(),
|
||||
cli_config: {
|
||||
host: 'walnut',
|
||||
node_version: 'v22.14.0',
|
||||
model: 'gemini-2.5-pro',
|
||||
specialization: 'general_ai',
|
||||
command_timeout: 60,
|
||||
ssh_timeout: 5
|
||||
},
|
||||
capabilities: ['Advanced Reasoning', 'General AI', 'Multi-modal'],
|
||||
metrics: {
|
||||
tasks_completed: 12,
|
||||
uptime: '4h 23m',
|
||||
response_time: 3.1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ironwood-gemini',
|
||||
name: 'IRONWOOD-GEMINI',
|
||||
endpoint: 'cli://ironwood',
|
||||
model: 'gemini-2.5-pro',
|
||||
specialty: 'reasoning',
|
||||
status: 'available',
|
||||
agent_type: 'cli',
|
||||
max_concurrent: 2,
|
||||
current_tasks: 1,
|
||||
last_seen: new Date().toISOString(),
|
||||
cli_config: {
|
||||
host: 'ironwood',
|
||||
node_version: 'v22.17.0',
|
||||
model: 'gemini-2.5-pro',
|
||||
specialization: 'reasoning',
|
||||
command_timeout: 60,
|
||||
ssh_timeout: 5
|
||||
},
|
||||
capabilities: ['Complex Reasoning', 'Problem Solving', 'Analysis'],
|
||||
metrics: {
|
||||
tasks_completed: 8,
|
||||
uptime: '2h 15m',
|
||||
response_time: 2.7
|
||||
}
|
||||
}
|
||||
] as Agent[];
|
||||
}
|
||||
},
|
||||
refetchInterval: 30000 // Refresh every 30 seconds
|
||||
queryFn: () => agentApi.getAgents(),
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
retry: 2,
|
||||
retryDelay: 1000
|
||||
});
|
||||
|
||||
const handleRegisterAgent = async (e: React.FormEvent) => {
|
||||
@@ -301,6 +188,48 @@ export default function Agents() {
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state with connectivity warning
|
||||
if (agentsError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Agents</h1>
|
||||
<p className="text-gray-600">Manage AI agents in your distributed cluster</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border p-8">
|
||||
<div className="text-center">
|
||||
<XCircleIcon className="h-16 w-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Unable to Load Agents</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
There's a connectivity issue with the agent management service. Please check your connection and try again.
|
||||
</p>
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<BoltIcon className="h-4 w-4 mr-2" />
|
||||
Retry Connection
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRegisterPredefinedAgents}
|
||||
className="inline-flex items-center px-4 py-2 border border-purple-600 rounded-md text-sm font-medium text-purple-600 bg-white hover:bg-purple-50"
|
||||
>
|
||||
<CommandLineIcon className="h-4 w-4 mr-2" />
|
||||
Quick Setup CLI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure agents is an array before using filter/reduce
|
||||
const agentsArray = Array.isArray(agents) ? agents : [];
|
||||
const onlineAgents = agentsArray.filter((agent: Agent) => agent.status === 'online' || agent.status === 'available').length;
|
||||
@@ -395,7 +324,34 @@ export default function Agents() {
|
||||
|
||||
{/* Agent Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{agents.map((agent: Agent) => (
|
||||
{agents.length === 0 ? (
|
||||
<div className="col-span-full">
|
||||
<div className="text-center py-12 bg-white rounded-lg border">
|
||||
<ComputerDesktopIcon className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Agents Registered</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Get started by registering your first AI agent. You can add Ollama or CLI-based agents to your cluster.
|
||||
</p>
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button
|
||||
onClick={handleRegisterPredefinedAgents}
|
||||
className="inline-flex items-center px-4 py-2 border border-purple-600 rounded-md text-sm font-medium text-purple-600 bg-white hover:bg-purple-50"
|
||||
>
|
||||
<CommandLineIcon className="h-4 w-4 mr-2" />
|
||||
Quick Setup CLI
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowRegistrationForm(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Register Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
agents.map((agent: Agent) => (
|
||||
<div key={agent.id} className="bg-white rounded-lg border p-6 hover:shadow-lg transition-shadow">
|
||||
{/* Agent Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -478,7 +434,8 @@ export default function Agents() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Registration Form Modal */}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ResponsiveContainer
|
||||
} from 'recharts';
|
||||
import { executionApi } from '../services/api';
|
||||
import { apiConfig } from '../config/api';
|
||||
|
||||
interface MetricsData {
|
||||
timestamp: string;
|
||||
@@ -50,28 +51,31 @@ interface SystemAlert {
|
||||
export default function Analytics() {
|
||||
const [timeRange, setTimeRange] = useState('24h');
|
||||
|
||||
// Future: Real-time metrics will be fetched here
|
||||
// const { data: clusterMetrics } = useQuery({
|
||||
// queryKey: ['cluster-metrics'],
|
||||
// queryFn: () => clusterApi.getMetrics(),
|
||||
// refetchInterval: 30000
|
||||
// });
|
||||
//
|
||||
// const { data: systemStatus } = useQuery({
|
||||
// queryKey: ['system-status'],
|
||||
// queryFn: () => systemApi.getStatus(),
|
||||
// refetchInterval: 10000
|
||||
// });
|
||||
|
||||
// Fetch recent executions for analytics
|
||||
const { data: executions = [] } = useQuery({
|
||||
const { data: executions = [], isLoading: executionsLoading, error: executionsError } = useQuery({
|
||||
queryKey: ['executions-analytics'],
|
||||
queryFn: () => executionApi.getExecutions(),
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
// Generate mock time series data for demonstration
|
||||
const generateTimeSeriesData = (): MetricsData[] => {
|
||||
// Try to fetch system metrics (will fail gracefully if not available)
|
||||
const { data: systemMetrics, isLoading: metricsLoading, error: metricsError } = useQuery({
|
||||
queryKey: ['system-metrics', timeRange],
|
||||
queryFn: async () => {
|
||||
// This will fail until we implement the backend endpoint
|
||||
const response = await fetch(`${apiConfig.baseURL}/api/monitoring/metrics?range=${timeRange}`);
|
||||
if (!response.ok) throw new Error('Metrics not available');
|
||||
return response.json();
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
retry: false
|
||||
});
|
||||
|
||||
// Generate time series data from actual executions if available, otherwise show unavailable message
|
||||
const generateTimeSeriesFromExecutions = (): MetricsData[] | null => {
|
||||
if (!executions.length || executionsError) return null;
|
||||
|
||||
// Group executions by time intervals based on timeRange
|
||||
const data: MetricsData[] = [];
|
||||
const now = new Date();
|
||||
const hours = timeRange === '24h' ? 24 : timeRange === '7d' ? 168 : 720;
|
||||
@@ -79,20 +83,28 @@ export default function Analytics() {
|
||||
|
||||
for (let i = hours; i >= 0; i -= interval) {
|
||||
const timestamp = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||
const intervalEnd = new Date(now.getTime() - (i - interval) * 60 * 60 * 1000);
|
||||
|
||||
// Filter executions in this time interval
|
||||
const intervalExecutions = executions.filter((exec: any) => {
|
||||
const execTime = new Date(exec.started_at);
|
||||
return execTime >= timestamp && execTime < intervalEnd;
|
||||
});
|
||||
|
||||
data.push({
|
||||
timestamp: timestamp.toISOString(),
|
||||
cpu_usage: Math.random() * 80 + 10,
|
||||
memory_usage: Math.random() * 70 + 20,
|
||||
active_executions: Math.floor(Math.random() * 10) + 1,
|
||||
completed_executions: Math.floor(Math.random() * 50) + 10,
|
||||
failed_executions: Math.floor(Math.random() * 5),
|
||||
response_time: Math.random() * 3 + 0.5
|
||||
cpu_usage: 0, // Will be 0 until backend provides this
|
||||
memory_usage: 0, // Will be 0 until backend provides this
|
||||
active_executions: intervalExecutions.filter((e: any) => e.status === 'running').length,
|
||||
completed_executions: intervalExecutions.filter((e: any) => e.status === 'completed').length,
|
||||
failed_executions: intervalExecutions.filter((e: any) => e.status === 'failed').length,
|
||||
response_time: 0 // Will be 0 until backend provides this
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const [timeSeriesData] = useState(() => generateTimeSeriesData());
|
||||
const timeSeriesData = generateTimeSeriesFromExecutions();
|
||||
|
||||
// Calculate execution analytics - ensure executions is an array
|
||||
const executionsArray = Array.isArray(executions) ? executions : [];
|
||||
@@ -114,35 +126,63 @@ export default function Analytics() {
|
||||
].filter(item => item.value > 0);
|
||||
|
||||
// Performance trends data
|
||||
const performanceData = timeSeriesData.slice(-7).map((item, index) => ({
|
||||
day: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][index],
|
||||
const performanceData = timeSeriesData ? timeSeriesData.slice(-7).map((item, index) => ({
|
||||
day: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][index % 7],
|
||||
executions: item.completed_executions,
|
||||
response_time: item.response_time,
|
||||
success_rate: Math.random() * 20 + 80
|
||||
}));
|
||||
success_rate: item.completed_executions > 0 ?
|
||||
((item.completed_executions / (item.completed_executions + item.failed_executions)) * 100) : 0
|
||||
})) : [];
|
||||
|
||||
// System alerts (mock data)
|
||||
const systemAlerts: SystemAlert[] = [
|
||||
{
|
||||
id: 'alert-1',
|
||||
type: 'warning',
|
||||
message: 'High memory usage on WALNUT node (85%)',
|
||||
timestamp: new Date(Date.now() - 1800000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
type: 'info',
|
||||
message: 'ACACIA node reconnected successfully',
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||||
resolved: true
|
||||
},
|
||||
{
|
||||
id: 'alert-3',
|
||||
// Generate system alerts from actual data
|
||||
const systemAlerts: SystemAlert[] = [];
|
||||
|
||||
// Add connectivity alerts if there are API errors
|
||||
if (executionsError) {
|
||||
systemAlerts.push({
|
||||
id: 'executions-error',
|
||||
type: 'error',
|
||||
message: 'Workflow execution failed: timeout after 5 minutes',
|
||||
timestamp: new Date(Date.now() - 7200000).toISOString()
|
||||
}
|
||||
];
|
||||
message: 'Unable to fetch execution data - API connectivity issue',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
if (metricsError) {
|
||||
systemAlerts.push({
|
||||
id: 'metrics-error',
|
||||
type: 'warning',
|
||||
message: 'System metrics unavailable - Monitoring service not configured',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Add alerts from failed executions
|
||||
if (executionsArray.length > 0) {
|
||||
const recentFailures = executionsArray
|
||||
.filter((exec: any) => exec.status === 'failed' && exec.completed_at)
|
||||
.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
|
||||
.slice(0, 3); // Show latest 3 failures
|
||||
|
||||
recentFailures.forEach((exec: any, index: number) => {
|
||||
systemAlerts.push({
|
||||
id: `exec-failure-${exec.id}`,
|
||||
type: 'error',
|
||||
message: `Execution failed: ${exec.workflow_name || exec.id} - ${exec.error || 'Unknown error'}`,
|
||||
timestamp: exec.completed_at
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If no alerts, show system operational message
|
||||
if (systemAlerts.length === 0) {
|
||||
systemAlerts.push({
|
||||
id: 'system-ok',
|
||||
type: 'info',
|
||||
message: 'All systems operational',
|
||||
timestamp: new Date().toISOString(),
|
||||
resolved: true
|
||||
});
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -250,8 +290,9 @@ export default function Analytics() {
|
||||
{/* Execution Trends */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Execution Trends</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
{timeSeriesData && timeSeriesData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
@@ -280,45 +321,68 @@ export default function Analytics() {
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[300px] bg-gray-50 rounded-lg">
|
||||
<div className="text-center">
|
||||
<ExclamationTriangleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 font-medium">Execution data unavailable</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{executionsError ? 'API connectivity issue' : 'No execution data found'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Resource Usage */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Resource Usage</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatTimestamp}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis domain={[0, 100]} />
|
||||
<Tooltip
|
||||
labelFormatter={(value) => formatTimestamp(value as string)}
|
||||
formatter={(value: any, name: string) => [`${Math.round(value)}%`, name === 'cpu_usage' ? 'CPU' : 'Memory']}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpu_usage"
|
||||
stackId="1"
|
||||
stroke="#3B82F6"
|
||||
fill="#3B82F6"
|
||||
fillOpacity={0.3}
|
||||
name="CPU Usage"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memory_usage"
|
||||
stackId="2"
|
||||
stroke="#8B5CF6"
|
||||
fill="#8B5CF6"
|
||||
fillOpacity={0.3}
|
||||
name="Memory Usage"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
{timeSeriesData && timeSeriesData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatTimestamp}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis domain={[0, 100]} />
|
||||
<Tooltip
|
||||
labelFormatter={(value) => formatTimestamp(value as string)}
|
||||
formatter={(value: any, name: string) => [`${Math.round(value)}%`, name === 'cpu_usage' ? 'CPU' : 'Memory']}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpu_usage"
|
||||
stackId="1"
|
||||
stroke="#3B82F6"
|
||||
fill="#3B82F6"
|
||||
fillOpacity={0.3}
|
||||
name="CPU Usage"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memory_usage"
|
||||
stackId="2"
|
||||
stroke="#8B5CF6"
|
||||
fill="#8B5CF6"
|
||||
fillOpacity={0.3}
|
||||
name="Memory Usage"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[300px] bg-gray-50 rounded-lg">
|
||||
<div className="text-center">
|
||||
<CpuChipIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 font-medium">System metrics unavailable</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Resource monitoring not configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -326,37 +390,61 @@ export default function Analytics() {
|
||||
{/* Execution Status Distribution */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Execution Status</h3>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={executionDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{executionDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
{executionDistribution.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={executionDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{executionDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[250px] bg-gray-50 rounded-lg">
|
||||
<div className="text-center">
|
||||
<ChartBarIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 font-medium">No execution data</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{executionsError ? 'Unable to load executions' : 'No executions found'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Performance Trends */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Weekly Performance</h3>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={performanceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="day" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="executions" fill="#3B82F6" name="Executions" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
{performanceData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={performanceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="day" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="executions" fill="#3B82F6" name="Executions" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[250px] bg-gray-50 rounded-lg">
|
||||
<div className="text-center">
|
||||
<ArrowTrendingUpIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 font-medium">Performance data unavailable</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Insufficient historical data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Alerts */}
|
||||
@@ -393,28 +481,40 @@ export default function Analytics() {
|
||||
{/* Response Time Trends */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Response Time Trends</h3>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatTimestamp}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis domain={[0, 'dataMax']} />
|
||||
<Tooltip
|
||||
labelFormatter={(value) => formatTimestamp(value as string)}
|
||||
formatter={(value: any) => [`${value.toFixed(2)}s`, 'Response Time']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="response_time"
|
||||
stroke="#F59E0B"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{timeSeriesData && timeSeriesData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatTimestamp}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis domain={[0, 'dataMax']} />
|
||||
<Tooltip
|
||||
labelFormatter={(value) => formatTimestamp(value as string)}
|
||||
formatter={(value: any) => [`${value.toFixed(2)}s`, 'Response Time']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="response_time"
|
||||
stroke="#F59E0B"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[200px] bg-gray-50 rounded-lg">
|
||||
<div className="text-center">
|
||||
<ClockIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 font-medium">Response time data unavailable</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Performance monitoring not configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
513
frontend/src/pages/BzzzChat.tsx
Normal file
513
frontend/src/pages/BzzzChat.tsx
Normal file
@@ -0,0 +1,513 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
PaperAirplaneIcon,
|
||||
MagnifyingGlassIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PhoneIcon,
|
||||
VideoCameraIcon,
|
||||
InformationCircleIcon,
|
||||
WifiIcon,
|
||||
ExclamationTriangleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface BzzzMessage {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
receiverId?: string;
|
||||
receiverName?: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
messageType: 'sent' | 'received' | 'system';
|
||||
channel: string;
|
||||
swarmId?: string;
|
||||
isDelivered: boolean;
|
||||
isRead: boolean;
|
||||
logType?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
interface BzzzChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
participants: string[];
|
||||
lastMessage?: BzzzMessage;
|
||||
unreadCount: number;
|
||||
isActive: boolean;
|
||||
swarmId: string;
|
||||
}
|
||||
|
||||
interface BzzzAgent {
|
||||
agent_id: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export default function BzzzChat() {
|
||||
const [channels, setChannels] = useState<BzzzChannel[]>([]);
|
||||
const [selectedChannel, setSelectedChannel] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<BzzzMessage[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [agents, setAgents] = useState<BzzzAgent[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// Connect to WebSocket for real-time updates
|
||||
useEffect(() => {
|
||||
const connectWebSocket = () => {
|
||||
try {
|
||||
const wsUrl = `ws://${window.location.host}/api/bzzz/logs/stream`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('🔗 Connected to Bzzz log stream');
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'initial_logs') {
|
||||
const logs = data.messages || [];
|
||||
processLogMessages(logs);
|
||||
} else if (data.type === 'new_messages') {
|
||||
const newMessages = data.messages || [];
|
||||
processLogMessages(newMessages, true);
|
||||
} else if (data.type === 'heartbeat') {
|
||||
// Send pong response
|
||||
ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('🔌 Disconnected from Bzzz log stream');
|
||||
setIsConnected(false);
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setError('Failed to connect to Bzzz log stream');
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
wsRef.current = ws;
|
||||
} catch (e) {
|
||||
console.error('Failed to create WebSocket connection:', e);
|
||||
setError('Failed to initialize real-time connection');
|
||||
}
|
||||
};
|
||||
|
||||
connectWebSocket();
|
||||
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch agents and initial data
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch available Bzzz agents
|
||||
const agentsResponse = await fetch('/api/bzzz/agents');
|
||||
if (agentsResponse.ok) {
|
||||
const agentsData = await agentsResponse.json();
|
||||
setAgents(agentsData.agents || []);
|
||||
}
|
||||
|
||||
// Fetch recent logs if WebSocket isn't connected yet
|
||||
if (!isConnected) {
|
||||
const logsResponse = await fetch('/api/bzzz/logs?limit=100');
|
||||
if (logsResponse.ok) {
|
||||
const logsData = await logsResponse.json();
|
||||
processLogMessages(logsData.logs || []);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch initial data:', e);
|
||||
setError('Failed to load Bzzz data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialData();
|
||||
}, [isConnected]);
|
||||
|
||||
const processLogMessages = (logs: any[], isNewMessage = false) => {
|
||||
// Group messages by channel/topic
|
||||
const channelMap = new Map<string, BzzzChannel>();
|
||||
const messageMap = new Map<string, BzzzMessage[]>();
|
||||
|
||||
logs.forEach((log: any) => {
|
||||
const channelId = log.channel || 'unknown';
|
||||
const message: BzzzMessage = {
|
||||
id: log.id || `log-${Date.now()}-${Math.random()}`,
|
||||
senderId: log.senderId || log.agent_id || 'unknown',
|
||||
senderName: log.senderName || log.senderId || log.agent_id || 'Unknown',
|
||||
content: log.content || 'No content',
|
||||
timestamp: log.timestamp,
|
||||
messageType: log.messageType || 'received',
|
||||
channel: channelId,
|
||||
swarmId: log.swarmId,
|
||||
isDelivered: log.isDelivered !== false,
|
||||
isRead: log.isRead !== false,
|
||||
logType: log.logType,
|
||||
hash: log.hash
|
||||
};
|
||||
|
||||
// Create or update channel
|
||||
if (!channelMap.has(channelId)) {
|
||||
channelMap.set(channelId, {
|
||||
id: channelId,
|
||||
name: formatChannelName(channelId),
|
||||
participants: [],
|
||||
unreadCount: 0,
|
||||
isActive: true,
|
||||
swarmId: log.swarmId || `swarm-${channelId}`
|
||||
});
|
||||
}
|
||||
|
||||
const channel = channelMap.get(channelId)!;
|
||||
|
||||
// Add participant if not exists
|
||||
if (!channel.participants.includes(message.senderName)) {
|
||||
channel.participants.push(message.senderName);
|
||||
}
|
||||
|
||||
// Update last message
|
||||
channel.lastMessage = message;
|
||||
|
||||
// Add to message list
|
||||
if (!messageMap.has(channelId)) {
|
||||
messageMap.set(channelId, []);
|
||||
}
|
||||
messageMap.get(channelId)!.push(message);
|
||||
});
|
||||
|
||||
// Update channels state
|
||||
if (isNewMessage) {
|
||||
setChannels(prev => {
|
||||
const updated = [...prev];
|
||||
channelMap.forEach((newChannel, channelId) => {
|
||||
const existingIndex = updated.findIndex(c => c.id === channelId);
|
||||
if (existingIndex >= 0) {
|
||||
updated[existingIndex] = { ...updated[existingIndex], ...newChannel };
|
||||
} else {
|
||||
updated.push(newChannel);
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Add new messages to existing messages
|
||||
setMessages(prev => {
|
||||
const newMessages = Array.from(messageMap.values()).flat();
|
||||
return [...prev, ...newMessages].sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Initial load - replace all data
|
||||
setChannels(Array.from(channelMap.values()));
|
||||
setMessages(Array.from(messageMap.values()).flat().sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
));
|
||||
|
||||
// Auto-select first channel
|
||||
if (channelMap.size > 0 && !selectedChannel) {
|
||||
setSelectedChannel(Array.from(channelMap.keys())[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatChannelName = (channelId: string): string => {
|
||||
if (channelId === 'bzzz') return 'Bzzz Coordination';
|
||||
if (channelId === 'antennae') return 'Antennae Meta-Discussion';
|
||||
if (channelId === 'unknown') return 'Unknown Channel';
|
||||
return channelId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
};
|
||||
|
||||
const formatTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString: string) => {
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'now';
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${days}d`;
|
||||
};
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
const selectedChannelData = channels.find(c => c.id === selectedChannel);
|
||||
const channelMessages = messages.filter(m => m.channel === selectedChannel);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Loading Bzzz Network
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Connecting to hypercore logging system...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div className="text-center">
|
||||
<ExclamationTriangleIcon className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Connection Error
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
{error}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex bg-white dark:bg-gray-900">
|
||||
{/* Channels Sidebar */}
|
||||
<div className="w-80 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Bzzz Network
|
||||
</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<WifiIcon className={`w-4 h-4 ${isConnected ? 'text-green-500' : 'text-red-500'}`} />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{isConnected ? 'Live' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon className="h-4 w-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search channels..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-sm text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Channels List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{channels.length === 0 ? (
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No channels found. Waiting for Bzzz agents to come online...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
channels.map((channel) => (
|
||||
<div
|
||||
key={channel.id}
|
||||
onClick={() => setSelectedChannel(channel.id)}
|
||||
className={`p-4 border-b border-gray-100 dark:border-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
||||
selectedChannel === channel.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-gray-900 dark:text-white text-sm">
|
||||
{channel.name}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
{channel.lastMessage && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatRelativeTime(channel.lastMessage.timestamp)}
|
||||
</span>
|
||||
)}
|
||||
{channel.unreadCount > 0 && (
|
||||
<span className="bg-blue-500 text-white text-xs rounded-full px-2 py-1 min-w-[20px] text-center">
|
||||
{channel.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{channel.participants.length} participants • {channel.swarmId}
|
||||
</div>
|
||||
{channel.lastMessage && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 truncate">
|
||||
<span className="font-medium">{channel.lastMessage.senderName}:</span>{' '}
|
||||
{channel.lastMessage.content}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Area */}
|
||||
{selectedChannelData ? (
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Chat Header */}
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-gray-900 dark:text-white">
|
||||
{selectedChannelData.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{selectedChannelData.participants.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||||
<InformationCircleIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50 dark:bg-gray-900">
|
||||
{channelMessages.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
No messages in this channel yet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
channelMessages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex ${
|
||||
message.messageType === 'sent' ? 'justify-end' :
|
||||
message.messageType === 'system' ? 'justify-center' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
{message.messageType === 'system' ? (
|
||||
<div className="bg-gray-200 dark:bg-gray-700 px-3 py-1 rounded-full">
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 text-center">
|
||||
{message.content}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-2xl ${
|
||||
message.messageType === 'sent'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{message.messageType === 'received' && (
|
||||
<p className="text-xs font-medium mb-1 text-blue-600 dark:text-blue-400">
|
||||
{message.senderName}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm">{message.content}</p>
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span
|
||||
className={`text-xs ${
|
||||
message.messageType === 'sent'
|
||||
? 'text-blue-100'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{formatTime(message.timestamp)}
|
||||
</span>
|
||||
{message.logType && (
|
||||
<span className="text-xs opacity-50 ml-2">
|
||||
{message.logType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Message Input - Disabled for monitoring */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Monitoring mode - messages are read-only"
|
||||
disabled
|
||||
className="w-full bg-transparent border-0 text-gray-500 dark:text-gray-400 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled
|
||||
className="p-2 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
||||
>
|
||||
<PaperAirplaneIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
||||
🐝 Real-time monitoring of hypercore P2P network • {agents.length} agents detected
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">🐝</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Select a channel to monitor
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Choose a Bzzz hypercore channel to view real-time agent communications
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,8 @@ export default function Dashboard() {
|
||||
|
||||
const { data: projects = [] } = useQuery({
|
||||
queryKey: ['projects'],
|
||||
queryFn: () => projectApi.getProjects()
|
||||
queryFn: () => projectApi.getProjects(),
|
||||
select: (data) => Array.isArray(data) ? data : []
|
||||
});
|
||||
|
||||
const { data: clusterOverview } = useQuery({
|
||||
@@ -42,18 +43,22 @@ export default function Dashboard() {
|
||||
|
||||
const { data: workflows = [] } = useQuery({
|
||||
queryKey: ['workflows'],
|
||||
queryFn: () => clusterApi.getWorkflows()
|
||||
queryFn: () => clusterApi.getWorkflows(),
|
||||
select: (data) => Array.isArray(data) ? data : []
|
||||
});
|
||||
|
||||
// Calculate stats from real data
|
||||
// Calculate stats from real data (with safety checks)
|
||||
const projectsArray = Array.isArray(projects) ? projects : [];
|
||||
const workflowsArray = Array.isArray(workflows) ? workflows : [];
|
||||
|
||||
const stats = {
|
||||
projects: {
|
||||
total: projects.length,
|
||||
active: projects.filter(p => p.status === 'active').length
|
||||
total: projectsArray.length,
|
||||
active: projectsArray.filter(p => p.status === 'active').length
|
||||
},
|
||||
workflows: {
|
||||
total: workflows.length,
|
||||
active: workflows.filter((w: any) => w.active).length
|
||||
total: workflowsArray.length,
|
||||
active: workflowsArray.filter((w: any) => w.active).length
|
||||
},
|
||||
cluster: {
|
||||
total_nodes: clusterOverview?.total_nodes || 0,
|
||||
|
||||
@@ -34,66 +34,12 @@ export default function Executions() {
|
||||
const [selectedExecution, setSelectedExecution] = useState<WorkflowExecution | null>(null);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const { data: executions = [], isLoading, refetch } = useQuery({
|
||||
const { data: executions = [], isLoading, error: executionsError, refetch } = useQuery({
|
||||
queryKey: ['executions'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await executionApi.getExecutions();
|
||||
} catch (err) {
|
||||
// Return mock data if API fails
|
||||
return [
|
||||
{
|
||||
id: 'exec-001',
|
||||
workflow_id: 'wf-001',
|
||||
workflow_name: 'Customer Data Processing',
|
||||
status: 'completed',
|
||||
started_at: new Date(Date.now() - 3600000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 3300000).toISOString(),
|
||||
duration: 300,
|
||||
agent_id: 'walnut',
|
||||
output: { processed_records: 1250, status: 'success' }
|
||||
},
|
||||
{
|
||||
id: 'exec-002',
|
||||
workflow_id: 'wf-002',
|
||||
workflow_name: 'Document Analysis',
|
||||
status: 'running',
|
||||
started_at: new Date(Date.now() - 1800000).toISOString(),
|
||||
agent_id: 'ironwood'
|
||||
},
|
||||
{
|
||||
id: 'exec-003',
|
||||
workflow_id: 'wf-001',
|
||||
workflow_name: 'Customer Data Processing',
|
||||
status: 'failed',
|
||||
started_at: new Date(Date.now() - 7200000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 7000000).toISOString(),
|
||||
duration: 200,
|
||||
agent_id: 'acacia',
|
||||
error: 'Database connection timeout'
|
||||
},
|
||||
{
|
||||
id: 'exec-004',
|
||||
workflow_id: 'wf-003',
|
||||
workflow_name: 'Email Campaign',
|
||||
status: 'pending',
|
||||
started_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'exec-005',
|
||||
workflow_id: 'wf-002',
|
||||
workflow_name: 'Document Analysis',
|
||||
status: 'completed',
|
||||
started_at: new Date(Date.now() - 14400000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 14100000).toISOString(),
|
||||
duration: 300,
|
||||
agent_id: 'walnut',
|
||||
output: { documents_processed: 45, insights_extracted: 23 }
|
||||
}
|
||||
] as WorkflowExecution[];
|
||||
}
|
||||
},
|
||||
refetchInterval: 5000 // Refresh every 5 seconds for real-time updates
|
||||
queryFn: () => executionApi.getExecutions(),
|
||||
refetchInterval: 5000, // Refresh every 5 seconds for real-time updates
|
||||
retry: 2,
|
||||
retryDelay: 1000
|
||||
});
|
||||
|
||||
const handleExecutionAction = async (executionId: string, action: 'cancel' | 'retry') => {
|
||||
@@ -174,6 +120,35 @@ export default function Executions() {
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state with connectivity warning
|
||||
if (executionsError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Executions</h1>
|
||||
<p className="text-gray-600">Monitor and manage workflow executions</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border p-8">
|
||||
<div className="text-center">
|
||||
<XCircleIcon className="h-16 w-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Unable to Load Executions</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
There's a connectivity issue with the execution service. Please check your connection and try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<ArrowPathIcon className="h-4 w-4 mr-2" />
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
@@ -259,6 +234,15 @@ export default function Executions() {
|
||||
|
||||
{/* Executions Table */}
|
||||
<div className="bg-white rounded-lg border overflow-hidden">
|
||||
{executions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<ClockIcon className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Executions Found</h3>
|
||||
<p className="text-gray-600">
|
||||
No workflow executions have been started yet. Create and run a workflow to see executions here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
@@ -345,6 +329,7 @@ export default function Executions() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Execution Details Modal */}
|
||||
|
||||
@@ -31,53 +31,14 @@ export default function ExecutionsAdvanced() {
|
||||
const [selectedExecution, setSelectedExecution] = useState<WorkflowExecution | null>(null);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const { data: executions = [], isLoading, refetch } = useQuery({
|
||||
const { data: executions = [], isLoading, error: executionsError, refetch } = useQuery({
|
||||
queryKey: ['executions-analytics'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await executionApi.getExecutions();
|
||||
} catch (err) {
|
||||
// Return mock data if API fails
|
||||
return generateMockExecutions();
|
||||
}
|
||||
},
|
||||
refetchInterval: 5000 // Refresh every 5 seconds
|
||||
queryFn: () => executionApi.getExecutions(),
|
||||
refetchInterval: 5000, // Refresh every 5 seconds
|
||||
retry: 2,
|
||||
retryDelay: 1000
|
||||
});
|
||||
|
||||
const generateMockExecutions = (): WorkflowExecution[] => {
|
||||
const statuses: WorkflowExecution['status'][] = ['pending', 'running', 'completed', 'failed', 'cancelled'];
|
||||
const workflows = [
|
||||
'Customer Data Processing',
|
||||
'Machine Learning Pipeline',
|
||||
'Report Generation',
|
||||
'Data Validation',
|
||||
'System Backup',
|
||||
'Model Training',
|
||||
'API Integration Test',
|
||||
'Performance Analysis'
|
||||
];
|
||||
|
||||
return Array.from({ length: 50 }, (_, i) => {
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
const startTime = new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000);
|
||||
const duration = status === 'completed' || status === 'failed'
|
||||
? Math.floor(Math.random() * 3600)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: `exec-${String(i + 1).padStart(3, '0')}`,
|
||||
workflow_id: `wf-${String(i + 1).padStart(3, '0')}`,
|
||||
workflow_name: workflows[Math.floor(Math.random() * workflows.length)],
|
||||
status,
|
||||
started_at: startTime.toISOString(),
|
||||
completed_at: duration ? new Date(startTime.getTime() + duration * 1000).toISOString() : undefined,
|
||||
duration,
|
||||
agent_id: `agent-${Math.floor(Math.random() * 5) + 1}`,
|
||||
error: status === 'failed' ? 'Connection timeout' : undefined,
|
||||
output: status === 'completed' ? { processed: Math.floor(Math.random() * 1000) } : undefined
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: WorkflowExecution['status']) => {
|
||||
const iconClass = "h-4 w-4";
|
||||
@@ -264,6 +225,37 @@ export default function ExecutionsAdvanced() {
|
||||
}
|
||||
];
|
||||
|
||||
// Show error state with connectivity warning
|
||||
if (executionsError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Workflow Executions</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Monitor and manage workflow execution history with advanced filtering and sorting
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border p-8">
|
||||
<div className="text-center">
|
||||
<XCircleIcon className="h-16 w-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Unable to Load Execution History</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
There's a connectivity issue with the execution service. Please check your connection and try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<ArrowPathIcon className="h-4 w-4 mr-2" />
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
@@ -310,7 +302,7 @@ export default function ExecutionsAdvanced() {
|
||||
loading={isLoading}
|
||||
searchPlaceholder="Search executions..."
|
||||
pageSize={15}
|
||||
emptyMessage="No executions found"
|
||||
emptyMessage="No workflow executions have been started yet. Create and run a workflow to see execution history here."
|
||||
onRowClick={(execution) => {
|
||||
setSelectedExecution(execution);
|
||||
setShowDetails(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
import {
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
KeyIcon,
|
||||
ExclamationCircleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import HiveLogo from '../assets/Hive_symbol.png';
|
||||
|
||||
interface LoginCredentials {
|
||||
username: string;
|
||||
@@ -50,17 +52,26 @@ export default function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||
{/* Theme toggle */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="mx-auto h-16 w-16 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white text-2xl font-bold">H</span>
|
||||
<div className="mx-auto h-16 w-16 flex items-center justify-center">
|
||||
<img
|
||||
src={HiveLogo}
|
||||
alt="Hive Logo"
|
||||
className="h-16 w-16 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
Sign in to Hive
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Distributed AI Management Platform
|
||||
</p>
|
||||
</div>
|
||||
@@ -70,12 +81,12 @@ export default function Login() {
|
||||
<div className="space-y-4">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Username
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<UserIcon className="h-5 w-5 text-gray-400" />
|
||||
<UserIcon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
@@ -85,7 +96,7 @@ export default function Login() {
|
||||
required
|
||||
value={credentials.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||
className="appearance-none relative block w-full pl-10 pr-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="appearance-none relative block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-700 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</div>
|
||||
@@ -93,12 +104,12 @@ export default function Login() {
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<KeyIcon className="h-5 w-5 text-gray-400" />
|
||||
<KeyIcon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
@@ -108,14 +119,14 @@ export default function Login() {
|
||||
required
|
||||
value={credentials.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
className="appearance-none relative block w-full pl-10 pr-10 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="appearance-none relative block w-full pl-10 pr-10 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-700 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5" />
|
||||
@@ -130,16 +141,16 @@ export default function Login() {
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Authentication failed
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,15 +165,15 @@ export default function Login() {
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600 dark:bg-gray-700 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
<a href="#" className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
@@ -184,11 +195,11 @@ export default function Login() {
|
||||
</div>
|
||||
|
||||
{/* Demo Credentials */}
|
||||
<div className="rounded-md bg-blue-50 p-4">
|
||||
<div className="text-sm text-blue-800">
|
||||
<div className="rounded-md bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium">Demo Credentials:</p>
|
||||
<p>Username: <code className="bg-blue-100 px-1 rounded">admin</code></p>
|
||||
<p>Password: <code className="bg-blue-100 px-1 rounded">hiveadmin</code></p>
|
||||
<p>Username: <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">admin</code></p>
|
||||
<p>Password: <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">hiveadmin123</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
DocumentDuplicateIcon,
|
||||
EyeIcon,
|
||||
StarIcon,
|
||||
FolderIcon
|
||||
FolderIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ArrowPathIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid';
|
||||
import DataTable, { Column } from '../components/ui/DataTable';
|
||||
@@ -196,6 +198,36 @@ export default function WorkflowTemplates() {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// Show error state when templates service is not available
|
||||
if (templatesError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Workflow Templates</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Pre-built workflow templates to accelerate your development
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border p-8">
|
||||
<div className="text-center">
|
||||
<ExclamationTriangleIcon className="h-16 w-16 text-yellow-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Templates Service Not Available</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
The workflow templates service is not yet configured. This feature will be available in a future update.
|
||||
</p>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-4 mt-4">
|
||||
<p className="text-sm text-blue-800">
|
||||
<DocumentTextIcon className="h-4 w-4 inline mr-1" />
|
||||
In the meantime, you can create workflows manually using the Workflow Editor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = ['all', ...Array.from(new Set(templates.map(t => t.category)))];
|
||||
|
||||
const filteredTemplates = selectedCategory === 'all'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '../config/api';
|
||||
import { Project, CreateProjectRequest, UpdateProjectRequest, ProjectMetrics } from '../types/project';
|
||||
import { Workflow, WorkflowExecution } from '../types/workflow';
|
||||
|
||||
// Create axios instance with base configuration
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VITE_API_BASE_URL || 'https://hive.home.deepblack.cloud',
|
||||
baseURL: apiConfig.baseURL,
|
||||
timeout: apiConfig.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -206,25 +208,25 @@ export const executionApi = {
|
||||
export const agentApi = {
|
||||
// Get all agents (both Ollama and CLI)
|
||||
getAgents: async () => {
|
||||
const response = await api.get('/agents');
|
||||
return response.data;
|
||||
const response = await api.get('/api/agents');
|
||||
return response.data.agents || []; // Extract agents array from response
|
||||
},
|
||||
|
||||
// Get agent status
|
||||
getAgentStatus: async (id: string) => {
|
||||
const response = await api.get(`/agents/${id}/status`);
|
||||
const response = await api.get(`/api/agents/${id}/status`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register new Ollama agent
|
||||
registerAgent: async (agentData: any) => {
|
||||
const response = await api.post('/agents', agentData);
|
||||
const response = await api.post('/api/agents', agentData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// CLI Agent Management
|
||||
getCliAgents: async () => {
|
||||
const response = await api.get('/cli-agents/');
|
||||
const response = await api.get('/api/cli-agents/');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -240,13 +242,13 @@ export const agentApi = {
|
||||
command_timeout?: number;
|
||||
ssh_timeout?: number;
|
||||
}) => {
|
||||
const response = await api.post('/cli-agents/register', cliAgentData);
|
||||
const response = await api.post('/api/cli-agents/register', cliAgentData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register predefined CLI agents (walnut-gemini, ironwood-gemini)
|
||||
registerPredefinedCliAgents: async () => {
|
||||
const response = await api.post('/cli-agents/register-predefined');
|
||||
const response = await api.post('/api/cli-agents/register-predefined');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user