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:
anthonyrawlins
2025-08-01 02:20:56 +10:00
parent 1e81daaf18
commit b6bff318d9
740 changed files with 90022 additions and 279523 deletions

View File

@@ -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>
)
}

View File

@@ -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',
},

View File

@@ -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',
},

View File

@@ -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',
},

View File

@@ -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',
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -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>

View 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>
);
};

View 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;

View File

@@ -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 = {

View File

@@ -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 => {

View 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>
);
};

View File

@@ -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', () => {

View File

@@ -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 = () => {

View File

@@ -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;

View File

@@ -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 */}

View File

@@ -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>
);

View 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>
);
}

View File

@@ -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,

View File

@@ -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 */}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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'

View File

@@ -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;
},