Major WHOOSH system refactoring and feature enhancements

- Migrated from HIVE branding to WHOOSH across all components
- Enhanced backend API with new services: AI models, BZZZ integration, templates, members
- Added comprehensive testing suite with security, performance, and integration tests
- Improved frontend with new components for project setup, AI models, and team management
- Updated MCP server implementation with WHOOSH-specific tools and resources
- Enhanced deployment configurations with production-ready Docker setups
- Added comprehensive documentation and setup guides
- Implemented age encryption service and UCXL integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-08-27 08:34:48 +10:00
parent 0e9844ef13
commit 268214d971
399 changed files with 57390 additions and 2045 deletions

View File

@@ -1,5 +1,5 @@
VITE_API_BASE_URL=https://hive.home.deepblack.cloud
VITE_WS_BASE_URL=https://hive.home.deepblack.cloud
VITE_API_BASE_URL=https://whoosh.home.deepblack.cloud
VITE_WS_BASE_URL=https://whoosh.home.deepblack.cloud
VITE_ENABLE_DEBUG_MODE=true
VITE_LOG_LEVEL=debug
VITE_DEV_MODE=true

View File

@@ -1,11 +1,11 @@
# Hive Frontend Environment Configuration
# WHOOSH Frontend Environment Configuration
# API Configuration
VITE_API_BASE_URL=http://localhost:8087
VITE_WS_BASE_URL=ws://localhost:8087
# Application Configuration
VITE_APP_NAME=Hive
VITE_APP_NAME=WHOOSH
VITE_APP_VERSION=1.0.0
VITE_APP_DESCRIPTION=Unified Distributed AI Orchestration Platform
@@ -41,8 +41,8 @@ VITE_CHUNK_SIZE_WARNING_LIMIT=1000
VITE_BUNDLE_ANALYZER=false
# Production overrides (set these in production environment)
# VITE_API_BASE_URL=https://hive.home.deepblack.cloud
# VITE_WS_BASE_URL=wss://hive.home.deepblack.cloud
# VITE_API_BASE_URL=https://whoosh.home.deepblack.cloud
# VITE_WS_BASE_URL=wss://whoosh.home.deepblack.cloud
# VITE_ENABLE_DEBUG_MODE=false
# VITE_ENABLE_ANALYTICS=true
# VITE_LOG_LEVEL=warn

View File

@@ -5,4 +5,4 @@ REACT_APP_DISABLE_SOCKETIO=true
# REACT_APP_API_BASE_URL=http://localhost:8000
# Optional: Set custom SocketIO URL when re-enabling
# REACT_APP_SOCKETIO_URL=https://hive.home.deepblack.cloud
# REACT_APP_SOCKETIO_URL=https://whoosh.home.deepblack.cloud

View File

@@ -1,6 +1,6 @@
# Production Environment Configuration
VITE_API_BASE_URL=https://hive.home.deepblack.cloud
VITE_WS_BASE_URL=https://hive.home.deepblack.cloud
VITE_API_BASE_URL=https://whoosh.home.deepblack.cloud
VITE_WS_BASE_URL=https://whoosh.home.deepblack.cloud
VITE_DISABLE_SOCKETIO=true
VITE_ENABLE_DEBUG_MODE=false
VITE_LOG_LEVEL=warn

53
frontend/Dockerfile.prod Normal file
View File

@@ -0,0 +1,53 @@
# Production Dockerfile for WHOOSH Frontend
FROM node:18-alpine as builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Install curl for health checks
RUN apk add --no-cache curl
# Copy built application
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Set proper permissions (nginx user already exists)
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
# nginx.conf already configured for port 8080
# RUN sed -i 's/listen 80/listen 8080/' /etc/nginx/nginx.conf
# Switch to non-root user
USER nginx
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080 || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,6 +1,6 @@
# Frontend Testing Infrastructure
This document describes the testing setup for the Hive frontend application.
This document describes the testing setup for the WHOOSH frontend application.
## Overview

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because one or more lines are too long

529
frontend/dist/assets/index-DVsl2bkP.js vendored Normal file

File diff suppressed because one or more lines are too long

82
frontend/dist/index.html vendored Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/whoosh-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="WHOOSH - Unified Distributed AI Orchestration Platform for coordinating AI agents, managing workflows, and monitoring performance" />
<meta name="keywords" content="AI, orchestration, workflows, agents, distributed computing, automation" />
<meta name="author" content="WHOOSH Platform" />
<meta name="theme-color" content="#3b82f6" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:title" content="WHOOSH - Distributed AI Orchestration" />
<meta property="og:description" content="Unified platform for coordinating AI agents, managing workflows, and monitoring performance" />
<meta property="og:url" content="https://whoosh.home.deepblack.cloud" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="WHOOSH - Distributed AI Orchestration" />
<meta property="twitter:description" content="Unified platform for coordinating AI agents, managing workflows, and monitoring performance" />
<!-- Accessibility -->
<meta name="format-detection" content="telephone=no" />
<!-- Google Fonts - Fira Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<title>🐝 WHOOSH - Distributed AI Orchestration</title>
<style>
/* Loading styles for better UX */
#root {
min-height: 100vh;
}
/* Screen reader only class */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Focus visible styles */
.focus-visible:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<script type="module" crossorigin src="/assets/index-DVsl2bkP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CZWs29Ng.css">
</head>
<body>
<noscript>
<div style="text-align: center; padding: 2rem; font-family: sans-serif;">
<h1>JavaScript Required</h1>
<p>This application requires JavaScript to be enabled in your browser.</p>
<p>Please enable JavaScript and refresh the page to continue.</p>
</div>
</noscript>
<div id="root" role="main"></div>
</body>
</html>

View File

@@ -2,22 +2,22 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/hive-icon.svg" />
<link rel="icon" type="image/svg+xml" href="/whoosh-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Hive - Unified Distributed AI Orchestration Platform for coordinating AI agents, managing workflows, and monitoring performance" />
<meta name="description" content="WHOOSH - Unified Distributed AI Orchestration Platform for coordinating AI agents, managing workflows, and monitoring performance" />
<meta name="keywords" content="AI, orchestration, workflows, agents, distributed computing, automation" />
<meta name="author" content="Hive Platform" />
<meta name="author" content="WHOOSH Platform" />
<meta name="theme-color" content="#3b82f6" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Hive - Distributed AI Orchestration" />
<meta property="og:title" content="WHOOSH - Distributed AI Orchestration" />
<meta property="og:description" content="Unified platform for coordinating AI agents, managing workflows, and monitoring performance" />
<meta property="og:url" content="https://hive.home.deepblack.cloud" />
<meta property="og:url" content="https://whoosh.home.deepblack.cloud" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Hive - Distributed AI Orchestration" />
<meta property="twitter:title" content="WHOOSH - Distributed AI Orchestration" />
<meta property="twitter:description" content="Unified platform for coordinating AI agents, managing workflows, and monitoring performance" />
<!-- Accessibility -->
@@ -28,7 +28,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<title>🐝 Hive - Distributed AI Orchestration</title>
<title>🐝 WHOOSH - Distributed AI Orchestration</title>
<style>
/* Loading styles for better UX */

116
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,116 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json
image/svg+xml;
# Server block
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Handle React Router (SPA routing)
location / {
try_files $uri $uri/ /index.html;
}
# API proxy to backend
location /api/ {
proxy_pass http://whoosh_backend:8087/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Socket.IO proxy
location /socket.io/ {
proxy_pass http://whoosh_backend:8087/socket.io/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Static assets with caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Deny access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "hive-frontend",
"name": "whoosh-frontend",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,

View File

@@ -1,121 +1,127 @@
{
"hash": "f1b624b4",
"configHash": "7f7c4023",
"lockfileHash": "841a9674",
"browserHash": "dcbe84fd",
"hash": "8e501d19",
"configHash": "87e5b2f0",
"lockfileHash": "387328fb",
"browserHash": "d6dc7b1e",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "dbbb866e",
"fileHash": "de4400ff",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "fbdc8a89",
"fileHash": "ee06cf71",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "8f8d2b77",
"fileHash": "c79c572c",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "859469db",
"fileHash": "813d06e6",
"needsInterop": true
},
"@headlessui/react": {
"src": "../../@headlessui/react/dist/headlessui.esm.js",
"file": "@headlessui_react.js",
"fileHash": "b34f13f9",
"fileHash": "7d32a755",
"needsInterop": false
},
"@heroicons/react/24/outline": {
"src": "../../@heroicons/react/24/outline/esm/index.js",
"file": "@heroicons_react_24_outline.js",
"fileHash": "d0b54f59",
"fileHash": "f0ad41a6",
"needsInterop": false
},
"@heroicons/react/24/solid": {
"src": "../../@heroicons/react/24/solid/esm/index.js",
"file": "@heroicons_react_24_solid.js",
"fileHash": "9251ace3",
"fileHash": "b45abbda",
"needsInterop": false
},
"@hookform/resolvers/zod": {
"src": "../../@hookform/resolvers/zod/dist/zod.mjs",
"file": "@hookform_resolvers_zod.js",
"fileHash": "fdc55a47",
"fileHash": "32b56492",
"needsInterop": false
},
"@tanstack/react-query": {
"src": "../../@tanstack/react-query/build/modern/index.js",
"file": "@tanstack_react-query.js",
"fileHash": "aee7b3e0",
"fileHash": "f0f0f364",
"needsInterop": false
},
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "18373c39",
"fileHash": "58fb8d8c",
"needsInterop": false
},
"date-fns": {
"src": "../../date-fns/esm/index.js",
"file": "date-fns.js",
"fileHash": "59af0bb5",
"fileHash": "d7dbd0b6",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "e2df5280",
"fileHash": "1190ba26",
"needsInterop": true
},
"react-hook-form": {
"src": "../../react-hook-form/dist/index.esm.mjs",
"file": "react-hook-form.js",
"fileHash": "37b2bebd",
"fileHash": "8a7b40ce",
"needsInterop": false
},
"react-hot-toast": {
"src": "../../react-hot-toast/dist/index.mjs",
"file": "react-hot-toast.js",
"fileHash": "7242ba44",
"fileHash": "1043bf79",
"needsInterop": false
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "21a4b678",
"fileHash": "b21df594",
"needsInterop": false
},
"reactflow": {
"src": "../../reactflow/dist/esm/index.mjs",
"file": "reactflow.js",
"fileHash": "a9a25ccf",
"fileHash": "ba4f988c",
"needsInterop": false
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "c76e306f",
"fileHash": "c9dd7a26",
"needsInterop": false
},
"socket.io-client": {
"src": "../../socket.io-client/build/esm/index.js",
"file": "socket__io-client.js",
"fileHash": "412a3e34",
"fileHash": "f920bd10",
"needsInterop": false
},
"zod": {
"src": "../../zod/index.js",
"file": "zod.js",
"fileHash": "c9a839cf",
"fileHash": "3f52d489",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "57855375",
"needsInterop": false
}
},
@@ -126,12 +132,12 @@
"chunk-Z4GA6XCR": {
"file": "chunk-Z4GA6XCR.js"
},
"chunk-WERSD76P": {
"file": "chunk-WERSD76P.js"
},
"chunk-S77I6LSE": {
"file": "chunk-S77I6LSE.js"
},
"chunk-WERSD76P": {
"file": "chunk-WERSD76P.js"
},
"chunk-PDF5LE6H": {
"file": "chunk-PDF5LE6H.js"
},

34825
frontend/node_modules/.vite/deps/lucide-react.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
frontend/node_modules/.vite/deps/lucide-react.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
{
"name": "hive-frontend",
"name": "whoosh-frontend",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hive-frontend",
"name": "whoosh-frontend",
"version": "1.1.0",
"dependencies": {
"@headlessui/react": "^1.7.17",

View File

@@ -1,7 +1,7 @@
{
"name": "hive-frontend",
"name": "whoosh-frontend",
"version": "1.1.0",
"description": "Hive Distributed AI Orchestration Platform - Frontend",
"description": "WHOOSH Distributed AI Orchestration Platform - Frontend",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -6,6 +6,7 @@ import { SocketIOProvider } from './contexts/SocketIOContext'
import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import ProtectedRoute from './components/auth/ProtectedRoute'
import ClusterDetector from './components/setup/ClusterDetector'
import Login from './pages/Login'
import UserProfile from './components/auth/UserProfile'
import Settings from './pages/Settings'
@@ -21,6 +22,9 @@ import WorkflowEditor from './components/workflows/WorkflowEditor'
import WorkflowDashboard from './components/workflows/WorkflowDashboard'
import ClusterNodes from './components/cluster/ClusterNodes'
import BzzzChat from './pages/BzzzChat'
import BzzzTeam from './pages/BzzzTeam'
import AIModels from './pages/AIModels'
import GitRepositories from './pages/GitRepositories'
function App() {
// Check for connection issues and provide fallback
@@ -132,6 +136,24 @@ function App() {
</ProtectedRoute>
} />
{/* AI Models */}
<Route path="/ai-models" element={
<ProtectedRoute>
<Layout>
<AIModels />
</Layout>
</ProtectedRoute>
} />
{/* Git Repositories */}
<Route path="/git-repositories" element={
<ProtectedRoute>
<Layout>
<GitRepositories />
</Layout>
</ProtectedRoute>
} />
{/* Bzzz Chat */}
<Route path="/bzzz-chat" element={
<ProtectedRoute>
@@ -141,6 +163,15 @@ function App() {
</ProtectedRoute>
} />
{/* Bzzz Team */}
<Route path="/bzzz-team" element={
<ProtectedRoute>
<Layout>
<BzzzTeam />
</Layout>
</ProtectedRoute>
} />
{/* Executions */}
<Route path="/executions" element={
<ProtectedRoute>
@@ -185,17 +216,19 @@ function App() {
return (
<Router>
<ThemeProvider>
<AuthProvider>
<ReactFlowProvider>
{socketIOEnabled ? (
<SocketIOProvider>
<ClusterDetector>
<AuthProvider>
<ReactFlowProvider>
{socketIOEnabled ? (
<SocketIOProvider>
<AppContent />
</SocketIOProvider>
) : (
<AppContent />
</SocketIOProvider>
) : (
<AppContent />
)}
</ReactFlowProvider>
</AuthProvider>
)}
</ReactFlowProvider>
</AuthProvider>
</ClusterDetector>
</ThemeProvider>
</Router>
)

View File

@@ -117,7 +117,7 @@ export interface APIError {
// Unified API configuration
export const API_CONFIG = {
BASE_URL: process.env.VITE_API_BASE_URL || 'https://api.hive.home.deepblack.cloud',
BASE_URL: process.env.VITE_API_BASE_URL || 'https://api.whoosh.home.deepblack.cloud',
TIMEOUT: 30000,
RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000,

View File

@@ -87,7 +87,7 @@ export class WebSocketService {
return;
}
const baseURL = import.meta.env.VITE_WS_BASE_URL || 'https://hive.home.deepblack.cloud';
const baseURL = import.meta.env.VITE_WS_BASE_URL || 'https://whoosh.home.deepblack.cloud';
this.socket = io(baseURL, {
auth: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -13,12 +13,14 @@ import {
UserCircleIcon,
ChevronDownIcon,
AdjustmentsHorizontalIcon,
ChatBubbleLeftRightIcon
ChatBubbleLeftRightIcon,
CpuChipIcon
} from '@heroicons/react/24/outline';
import { GitBranch } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import UserProfile from './auth/UserProfile';
import { ThemeToggle } from './ThemeToggle';
import HiveLogo from '../assets/Hive_symbol.png';
import WHOOSHLogo from '../assets/WHOOSH_symbol.png';
interface NavigationItem {
name: string;
@@ -30,11 +32,14 @@ interface NavigationItem {
const navigation: NavigationItem[] = [
{ name: 'Dashboard', href: '/', icon: HomeIcon },
{ name: 'Projects', href: '/projects', icon: FolderIcon },
{ name: 'Git Repositories', href: '/git-repositories', icon: GitBranch },
{ name: 'Workflows', href: '/workflows', icon: Cog6ToothIcon },
{ name: 'Cluster', href: '/cluster', icon: ComputerDesktopIcon },
{ name: 'Executions', href: '/executions', icon: PlayIcon },
{ name: 'Agents', href: '/agents', icon: UserGroupIcon },
{ name: 'AI Models', href: '/ai-models', icon: CpuChipIcon },
{ name: 'Bzzz Chat', href: '/bzzz-chat', icon: ChatBubbleLeftRightIcon },
{ name: 'Bzzz Team', href: '/bzzz-team', icon: UserGroupIcon },
{ name: 'Analytics', href: '/analytics', icon: ChartBarIcon },
{ name: 'Settings', href: '/settings', icon: AdjustmentsHorizontalIcon },
];
@@ -82,8 +87,8 @@ export default function Layout({ children }: LayoutProps) {
<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">
<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>
<img src={WHOOSHLogo} alt="WHOOSH" className="h-8 w-8 object-contain" />
<span className="text-lg font-semibold text-gray-900 dark:text-white">WHOOSH</span>
</div>
<button
onClick={() => setSidebarOpen(false)}
@@ -119,8 +124,8 @@ export default function Layout({ children }: LayoutProps) {
<div className="hidden lg:flex lg:flex-shrink-0">
<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>
<img src={WHOOSHLogo} alt="WHOOSH" className="h-8 w-8 object-contain mr-2" />
<span className="text-xl font-semibold text-gray-900 dark:text-white">WHOOSH</span>
</div>
<nav className="flex-1 px-4 py-4 space-y-1">
{navigationWithCurrent.map((item) => (
@@ -165,7 +170,7 @@ export default function Layout({ children }: LayoutProps) {
</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 dark:text-white">Hive</span>
<span className="text-lg font-semibold text-gray-900 dark:text-white">WHOOSH</span>
</div>
</div>

View File

@@ -211,7 +211,7 @@ export const APIKeyManager: React.FC = () => {
<div>
<h2 className="text-2xl font-bold text-gray-900">API Keys</h2>
<p className="text-gray-600 mt-1">
Manage API keys for programmatic access to Hive
Manage API keys for programmatic access to WHOOSH
</p>
</div>
@@ -349,7 +349,7 @@ export const APIKeyManager: React.FC = () => {
<Key className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No API Keys</h3>
<p className="text-gray-600 mb-4">
Create your first API key to start using the Hive API programmatically.
Create your first API key to start using the WHOOSH API programmatically.
</p>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />

View File

@@ -72,7 +72,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess, redirectTo = '/
<Lock className="w-6 h-6 text-white" />
</div>
<CardTitle className="text-2xl font-bold text-gray-900">
Welcome to Hive
Welcome to WHOOSH
</CardTitle>
<p className="text-gray-600 mt-2">
Sign in to your account to continue

View File

@@ -0,0 +1,264 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
UserGroupIcon,
ChartBarIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
KeyIcon,
EnvelopeIcon
} from '@heroicons/react/24/outline';
import MemberList from './MemberList';
import MemberInviteForm from './MemberInviteForm';
interface MemberDashboardProps {
projectId: string;
projectName: string;
}
interface MemberStats {
total_members: number;
active_members: number;
pending_invitations: number;
recent_activity: any[];
}
export default function MemberDashboard({ projectId, projectName }: MemberDashboardProps) {
const [showInviteForm, setShowInviteForm] = useState(false);
// Fetch member statistics
const { data: members } = useQuery({
queryKey: ['project-members', projectId],
queryFn: async () => {
const response = await fetch(`/api/members/projects/${projectId}`);
if (!response.ok) {
throw new Error('Failed to fetch project members');
}
return response.json();
}
});
// Calculate statistics from members data
const stats = {
total_members: members?.length || 0,
active_members: members?.filter((m: any) => m.status === 'accepted').length || 0,
pending_invitations: members?.filter((m: any) => m.status === 'pending').length || 0,
recent_activity: [] // Placeholder for activity feed
};
const handleInviteSuccess = () => {
setShowInviteForm(false);
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900">Team Management</h1>
<p className="text-gray-600 mt-1">
Manage team members, roles, and collaboration for <strong>{projectName}</strong>
</p>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white overflow-hidden shadow-sm rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<UserGroupIcon className="h-6 w-6 text-gray-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Total Members
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats.total_members}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow-sm rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<CheckCircleIcon className="h-6 w-6 text-green-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Active Members
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats.active_members}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow-sm rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<ClockIcon className="h-6 w-6 text-yellow-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Pending Invites
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats.pending_invitations}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow-sm rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<KeyIcon className="h-6 w-6 text-blue-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Age Encryption
</dt>
<dd className="text-lg font-medium text-gray-900">
{members?.filter((m: any) => m.age_access).length || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Quick Actions</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => setShowInviteForm(true)}
className="flex items-center p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<EnvelopeIcon className="h-6 w-6 text-gray-400 mr-3" />
<div className="text-left">
<p className="text-sm font-medium text-gray-900">Invite New Member</p>
<p className="text-xs text-gray-500">Send an invitation to join the team</p>
</div>
</button>
<button className="flex items-center p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-green-400 hover:bg-green-50 transition-colors">
<UserGroupIcon className="h-6 w-6 text-gray-400 mr-3" />
<div className="text-left">
<p className="text-sm font-medium text-gray-900">Bulk Import</p>
<p className="text-xs text-gray-500">Import multiple members from CSV</p>
</div>
</button>
<button className="flex items-center p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-purple-400 hover:bg-purple-50 transition-colors">
<ChartBarIcon className="h-6 w-6 text-gray-400 mr-3" />
<div className="text-left">
<p className="text-sm font-medium text-gray-900">Member Analytics</p>
<p className="text-xs text-gray-500">View detailed member statistics</p>
</div>
</button>
</div>
</div>
</div>
{/* Role Distribution Chart */}
{members && members.length > 0 && (
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Role Distribution</h3>
</div>
<div className="p-6">
<div className="space-y-4">
{['owner', 'maintainer', 'developer', 'viewer'].map((role) => {
const roleCount = members.filter((m: any) => m.role === role).length;
const percentage = members.length > 0 ? (roleCount / members.length) * 100 : 0;
const getRoleColor = (role: string) => {
switch (role) {
case 'owner': return 'bg-purple-500';
case 'maintainer': return 'bg-blue-500';
case 'developer': return 'bg-green-500';
case 'viewer': return 'bg-gray-500';
default: return 'bg-gray-500';
}
};
return (
<div key={role} className="flex items-center">
<div className="w-20 text-sm text-gray-600 capitalize">
{role}
</div>
<div className="flex-1 ml-4">
<div className="bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getRoleColor(role)}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
<div className="w-12 text-sm text-gray-600 text-right">
{roleCount}
</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Recent Activity */}
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Recent Activity</h3>
</div>
<div className="p-6">
{/* Placeholder for recent activity */}
<div className="text-center text-gray-500 py-8">
<ClockIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No recent activity</h3>
<p className="mt-1 text-sm text-gray-500">
Member activities will appear here once they start collaborating.
</p>
</div>
</div>
</div>
{/* Member List */}
<MemberList
projectId={projectId}
projectName={projectName}
onInviteMember={() => setShowInviteForm(true)}
/>
{/* Invite Form Modal */}
<MemberInviteForm
projectId={projectId}
projectName={projectName}
isOpen={showInviteForm}
onSuccess={handleInviteSuccess}
onCancel={() => setShowInviteForm(false)}
/>
</div>
);
}

View File

@@ -0,0 +1,322 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
PlusIcon,
XMarkIcon,
EnvelopeIcon,
KeyIcon,
InformationCircleIcon
} from '@heroicons/react/24/outline';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
// Schema for member invitation form
const memberInviteSchema = z.object({
project_id: z.string().min(1, 'Project ID is required'),
member_email: z.string().email('Valid email address is required'),
role: z.enum(['owner', 'maintainer', 'developer', 'viewer']),
custom_message: z.string().max(1000, 'Message must be less than 1000 characters').optional(),
send_email: z.boolean().default(true),
include_age_key: z.boolean().default(true)
});
type MemberInviteData = z.infer<typeof memberInviteSchema>;
interface MemberInviteFormProps {
projectId: string;
projectName: string;
onSuccess?: (invitation: any) => void;
onCancel?: () => void;
isOpen: boolean;
}
export default function MemberInviteForm({
projectId,
projectName,
onSuccess,
onCancel,
isOpen
}: MemberInviteFormProps) {
const queryClient = useQueryClient();
const [showAdvanced, setShowAdvanced] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch,
reset
} = useForm<MemberInviteData>({
resolver: zodResolver(memberInviteSchema),
defaultValues: {
project_id: projectId,
member_email: '',
role: 'developer',
custom_message: '',
send_email: true,
include_age_key: true
}
});
const selectedRole = watch('role');
const sendEmail = watch('send_email');
const includeAgeKey = watch('include_age_key');
const inviteMemberMutation = useMutation({
mutationFn: async (data: MemberInviteData) => {
const response = await fetch('/api/members/invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to send invitation');
}
return response.json();
},
onSuccess: (result) => {
toast.success(`Invitation sent to ${result.member_email}`);
queryClient.invalidateQueries({ queryKey: ['project-members', projectId] });
reset();
onSuccess?.(result);
},
onError: (error) => {
toast.error(`Failed to send invitation: ${error.message}`);
}
});
const onSubmit = (data: MemberInviteData) => {
inviteMemberMutation.mutate(data);
};
const roleDescriptions = {
owner: 'Full administrative access to the project',
maintainer: 'Write access with merge permissions and member management',
developer: 'Write access for development work and collaboration',
viewer: 'Read-only access to project resources'
};
const rolePermissions = {
owner: ['Admin', 'Write', 'Read', 'Delete', 'Manage Members', 'Configure Settings', 'Manage Age Keys'],
maintainer: ['Write', 'Read', 'Assign Issues', 'Merge PRs', 'Invite Members', 'Decrypt Age Data'],
developer: ['Write', 'Read', 'Create Issues', 'Create PRs', 'Decrypt Age Data'],
viewer: ['Read', 'View Issues', 'View PRs']
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-lg bg-white">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-medium text-gray-900">Invite Team Member</h3>
<p className="text-sm text-gray-500 mt-1">
Invite a new member to collaborate on <strong>{projectName}</strong>
</p>
</div>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Member Email */}
<div>
<label htmlFor="member_email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address *
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<EnvelopeIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="email"
id="member_email"
{...register('member_email')}
className="block w-full pl-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="colleague@company.com"
/>
</div>
{errors.member_email && (
<p className="mt-1 text-sm text-red-600">{errors.member_email.message}</p>
)}
</div>
{/* Role Selection */}
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700 mb-2">
Role *
</label>
<select
id="role"
{...register('role')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="developer">Developer</option>
<option value="maintainer">Maintainer</option>
<option value="viewer">Viewer</option>
<option value="owner">Owner</option>
</select>
{errors.role && (
<p className="mt-1 text-sm text-red-600">{errors.role.message}</p>
)}
{/* Role Description */}
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
<strong>{selectedRole.charAt(0).toUpperCase() + selectedRole.slice(1)}:</strong> {roleDescriptions[selectedRole]}
</p>
<div className="mt-2">
<p className="text-xs text-blue-700 font-medium">Permissions:</p>
<div className="flex flex-wrap gap-1 mt-1">
{rolePermissions[selectedRole].map((permission) => (
<span
key={permission}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
>
{permission}
</span>
))}
</div>
</div>
</div>
</div>
{/* Custom Message */}
<div>
<label htmlFor="custom_message" className="block text-sm font-medium text-gray-700 mb-2">
Personal Message (Optional)
</label>
<textarea
id="custom_message"
rows={3}
{...register('custom_message')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Add a personal welcome message for this team member..."
/>
<p className="mt-1 text-sm text-gray-500">
{watch('custom_message')?.length || 0}/1000 characters
</p>
{errors.custom_message && (
<p className="mt-1 text-sm text-red-600">{errors.custom_message.message}</p>
)}
</div>
{/* Advanced Options */}
<div>
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center text-sm text-blue-600 hover:text-blue-700"
>
{showAdvanced ? 'Hide' : 'Show'} Advanced Options
<InformationCircleIcon className="ml-1 h-4 w-4" />
</button>
{showAdvanced && (
<div className="mt-4 space-y-4 p-4 bg-gray-50 border border-gray-200 rounded-md">
{/* Send Email Option */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="send_email"
{...register('send_email')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<div>
<label htmlFor="send_email" className="text-sm font-medium text-gray-700">
Send email invitation
</label>
<p className="text-sm text-gray-500">
Automatically email the invitation to the member
</p>
</div>
</div>
{/* Include Age Key Option */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="include_age_key"
{...register('include_age_key')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<div>
<label htmlFor="include_age_key" className="text-sm font-medium text-gray-700 flex items-center">
<KeyIcon className="h-4 w-4 mr-1" />
Include Age encryption key
</label>
<p className="text-sm text-gray-500">
Attach the project's Age public key for secure communication
</p>
</div>
</div>
{/* Security Notice */}
{includeAgeKey && (
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-yellow-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Age Encryption Security
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The Age public key will be attached to the invitation email.
This allows the new member to encrypt data for this project.
</p>
<p className="mt-1">
Private key access for decryption will be managed separately based on their role.
</p>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Form Actions */}
<div className="flex justify-end space-x-4 pt-6 border-t border-gray-200">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
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 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="animate-spin -ml-1 mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
Sending Invitation...
</>
) : (
<>
<PlusIcon className="h-4 w-4 mr-2" />
{sendEmail ? 'Send Invitation' : 'Create Invitation'}
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,373 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
UserGroupIcon,
EnvelopeIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
EllipsisVerticalIcon,
ArrowPathIcon,
TrashIcon,
KeyIcon,
GitBranchIcon
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
interface ProjectMember {
email: string;
role: string;
status: string;
invited_at: string;
invited_by: string;
accepted_at?: string;
permissions: string[];
gitea_access: boolean;
age_access: boolean;
}
interface MemberListProps {
projectId: string;
projectName: string;
onInviteMember?: () => void;
}
export default function MemberList({ projectId, projectName, onInviteMember }: MemberListProps) {
const queryClient = useQueryClient();
const [selectedMember, setSelectedMember] = useState<string | null>(null);
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
// Fetch project members
const { data: members, isLoading, error } = useQuery({
queryKey: ['project-members', projectId],
queryFn: async () => {
const response = await fetch(`/api/members/projects/${projectId}`);
if (!response.ok) {
throw new Error('Failed to fetch project members');
}
return response.json() as ProjectMember[];
}
});
// Remove member mutation
const removeMemberMutation = useMutation({
mutationFn: async (memberEmail: string) => {
const response = await fetch(`/api/members/projects/${projectId}/members`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
member_email: memberEmail,
reason: 'Removed by project admin'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to remove member');
}
return response.json();
},
onSuccess: (_, memberEmail) => {
toast.success(`Member ${memberEmail} removed successfully`);
queryClient.invalidateQueries({ queryKey: ['project-members', projectId] });
setActionMenuOpen(null);
},
onError: (error) => {
toast.error(`Failed to remove member: ${error.message}`);
}
});
// Resend invitation mutation
const resendInvitationMutation = useMutation({
mutationFn: async (memberEmail: string) => {
// Find the invitation ID for this member
const member = members?.find(m => m.email === memberEmail);
if (!member) throw new Error('Member not found');
// For now, we'll assume invitation IDs follow a pattern
// In production, you'd store this properly
const invitationId = `inv_${projectId}_${memberEmail.replace('@', '_').replace('.', '_')}`;
const response = await fetch(`/api/members/projects/${projectId}/invitations/${invitationId}/resend`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to resend invitation');
}
return response.json();
},
onSuccess: (_, memberEmail) => {
toast.success(`Invitation resent to ${memberEmail}`);
setActionMenuOpen(null);
},
onError: (error) => {
toast.error(`Failed to resend invitation: ${error.message}`);
}
});
const getStatusIcon = (status: string) => {
switch (status) {
case 'accepted':
return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
case 'pending':
return <ClockIcon className="h-5 w-5 text-yellow-500" />;
case 'revoked':
return <XCircleIcon className="h-5 w-5 text-red-500" />;
default:
return <ClockIcon className="h-5 w-5 text-gray-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'accepted':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'revoked':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getRoleColor = (role: string) => {
switch (role) {
case 'owner':
return 'bg-purple-100 text-purple-800';
case 'maintainer':
return 'bg-blue-100 text-blue-800';
case 'developer':
return 'bg-green-100 text-green-800';
case 'viewer':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
if (isLoading) {
return (
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Team Members</h3>
</div>
<div className="p-6">
<div className="animate-pulse space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center space-x-4">
<div className="rounded-full bg-gray-200 h-10 w-10"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-1/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Team Members</h3>
</div>
<div className="p-6">
<div className="text-center text-red-600">
Failed to load team members: {error.message}
</div>
</div>
</div>
);
}
return (
<div className="bg-white shadow-sm rounded-lg">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<UserGroupIcon className="h-5 w-5 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900">Team Members</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{members?.length || 0}
</span>
</div>
{onInviteMember && (
<button
onClick={onInviteMember}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<EnvelopeIcon className="h-4 w-4 mr-2" />
Invite Member
</button>
)}
</div>
</div>
{/* Member List */}
<div className="divide-y divide-gray-200">
{!members || members.length === 0 ? (
<div className="p-6 text-center text-gray-500">
<UserGroupIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No team members</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by inviting your first team member.
</p>
{onInviteMember && (
<div className="mt-6">
<button
onClick={onInviteMember}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<EnvelopeIcon className="h-4 w-4 mr-2" />
Invite Team Member
</button>
</div>
)}
</div>
) : (
members.map((member) => (
<div key={member.email} className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{/* Avatar */}
<div className="flex-shrink-0">
<div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
<span className="text-sm font-medium text-gray-700">
{member.email.charAt(0).toUpperCase()}
</span>
</div>
</div>
{/* Member Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium text-gray-900 truncate">
{member.email}
</p>
{getStatusIcon(member.status)}
</div>
<div className="mt-1 flex items-center space-x-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getRoleColor(member.role)}`}>
{member.role.charAt(0).toUpperCase() + member.role.slice(1)}
</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(member.status)}`}>
{member.status.charAt(0).toUpperCase() + member.status.slice(1)}
</span>
</div>
<div className="mt-2 flex items-center space-x-4 text-xs text-gray-500">
<span>Invited by {member.invited_by}</span>
<span></span>
<span>{formatDate(member.invited_at)}</span>
{member.accepted_at && (
<>
<span></span>
<span>Joined {formatDate(member.accepted_at)}</span>
</>
)}
</div>
{/* Access Indicators */}
<div className="mt-2 flex items-center space-x-3">
<div className="flex items-center space-x-1">
<GitBranchIcon className={`h-4 w-4 ${member.gitea_access ? 'text-green-500' : 'text-gray-400'}`} />
<span className="text-xs text-gray-500">
{member.gitea_access ? 'GITEA Access' : 'No GITEA'}
</span>
</div>
<div className="flex items-center space-x-1">
<KeyIcon className={`h-4 w-4 ${member.age_access ? 'text-blue-500' : 'text-gray-400'}`} />
<span className="text-xs text-gray-500">
{member.age_access ? 'Age Encryption' : 'No Encryption'}
</span>
</div>
</div>
</div>
</div>
{/* Actions Menu */}
<div className="relative">
<button
onClick={() => setActionMenuOpen(actionMenuOpen === member.email ? null : member.email)}
className="p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
>
<EllipsisVerticalIcon className="h-5 w-5" />
</button>
{actionMenuOpen === member.email && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 border border-gray-200">
<div className="py-1">
{member.status === 'pending' && (
<button
onClick={() => resendInvitationMutation.mutate(member.email)}
disabled={resendInvitationMutation.isLoading}
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<ArrowPathIcon className="h-4 w-4 mr-2" />
Resend Invitation
</button>
)}
<button
onClick={() => {
if (confirm(`Are you sure you want to remove ${member.email} from this project?`)) {
removeMemberMutation.mutate(member.email);
}
}}
disabled={removeMemberMutation.isLoading}
className="flex items-center w-full px-4 py-2 text-sm text-red-700 hover:bg-red-50"
>
<TrashIcon className="h-4 w-4 mr-2" />
Remove Member
</button>
</div>
</div>
)}
</div>
</div>
{/* Expanded Details */}
{selectedMember === member.email && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<h4 className="text-sm font-medium text-gray-900 mb-3">Permissions</h4>
<div className="flex flex-wrap gap-2">
{member.permissions.map((permission) => (
<span
key={permission}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{permission.replace('_', ' ').replace('.', ': ').split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')}
</span>
))}
</div>
</div>
)}
</div>
))
)}
</div>
</div>
);
}

View File

@@ -182,7 +182,7 @@ export default function ProjectDetail() {
</button>
<button className="inline-flex items-center px-3 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50">
<TrashIcon className="h-4 w-4 mr-2" />
Archive
Arcwhoosh
</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -7,37 +7,92 @@ import {
ArrowLeftIcon,
XMarkIcon,
PlusIcon,
InformationCircleIcon
InformationCircleIcon,
CheckIcon,
ArrowRightIcon,
ClockIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
import toast from 'react-hot-toast';
const projectSchema = z.object({
// Updated schema to match the new GITEA-integrated project setup API
const projectSetupSchema = z.object({
// Basic Information
name: z.string().min(1, 'Project name is required').max(100, 'Name must be less than 100 characters'),
description: z.string().max(500, 'Description must be less than 500 characters').optional(),
tags: z.array(z.string()).optional(),
metadata: z.object({
owner: z.string().optional(),
department: z.string().optional(),
priority: z.enum(['low', 'medium', 'high']).optional()
template_id: z.string().optional(),
// Age Key Configuration
age_config: z.object({
generate_new_key: z.boolean().default(true),
master_key_passphrase: z.string().optional(),
key_backup_location: z.string().optional()
}).optional(),
// Git Configuration
git_config: z.object({
repo_type: z.enum(['new', 'existing', 'import']).default('new'),
repo_name: z.string().optional(),
git_url: z.string().optional(),
git_owner: z.string().default('whoosh'),
git_branch: z.string().default('main'),
auto_initialize: z.boolean().default(true),
private: z.boolean().default(false),
license_type: z.string().default('MIT')
}),
// BZZZ Configuration
bzzz_config: z.object({
git_url: z.string().url('Must be a valid Git URL').optional().or(z.literal('')),
git_owner: z.string().optional(),
git_repository: z.string().optional(),
git_branch: z.string().optional(),
bzzz_enabled: z.boolean().optional(),
ready_to_claim: z.boolean().optional(),
private_repo: z.boolean().optional(),
github_token_required: z.boolean().optional()
enable_bzzz: z.boolean().default(false),
task_coordination: z.boolean().default(true),
ai_agent_access: z.boolean().default(false),
auto_discovery: z.boolean().default(true)
}).optional(),
// Member Configuration
member_config: z.object({
initial_members: z.array(z.object({
email: z.string().email(),
role: z.enum(['owner', 'maintainer', 'developer', 'viewer']).default('developer')
})).optional()
}).optional(),
// Advanced Configuration
advanced_config: z.object({
project_visibility: z.enum(['private', 'internal', 'public']).default('private'),
security_level: z.enum(['standard', 'high', 'maximum']).default('standard'),
backup_enabled: z.boolean().default(true),
monitoring_enabled: z.boolean().default(true)
}).optional()
});
type ProjectFormData = z.infer<typeof projectSchema>;
type ProjectSetupData = z.infer<typeof projectSetupSchema>;
// Project setup steps
type SetupStep = 'basic' | 'template' | 'git' | 'age-keys' | 'bzzz' | 'members' | 'review';
// Setup progress tracking
interface SetupProgress {
step: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
message: string;
details?: any;
}
// Template interface
interface ProjectTemplate {
template_id: string;
name: string;
description: string;
icon: string;
features: string[];
}
interface ProjectFormProps {
mode: 'create' | 'edit';
initialData?: Partial<ProjectFormData>;
initialData?: Partial<ProjectSetupData>;
projectId?: string;
}
@@ -45,88 +100,159 @@ export default function ProjectForm({ mode, initialData, projectId }: ProjectFor
const navigate = useNavigate();
const queryClient = useQueryClient();
const [currentTag, setCurrentTag] = useState('');
const [currentStep, setCurrentStep] = useState<SetupStep>('basic');
const [setupProgress, setSetupProgress] = useState<SetupProgress[]>([]);
const [isCreatingProject, setIsCreatingProject] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch,
setValue
} = useForm<ProjectFormData>({
resolver: zodResolver(projectSchema),
setValue,
trigger
} = useForm<ProjectSetupData>({
resolver: zodResolver(projectSetupSchema),
mode: 'onChange',
defaultValues: {
name: initialData?.name || '',
description: initialData?.description || '',
tags: initialData?.tags || [],
metadata: {
owner: initialData?.metadata?.owner || '',
department: initialData?.metadata?.department || '',
priority: initialData?.metadata?.priority || 'medium'
template_id: initialData?.template_id || '',
age_config: {
generate_new_key: initialData?.age_config?.generate_new_key ?? true,
master_key_passphrase: initialData?.age_config?.master_key_passphrase || '',
key_backup_location: initialData?.age_config?.key_backup_location || ''
},
git_config: {
repo_type: initialData?.git_config?.repo_type || 'new',
repo_name: initialData?.git_config?.repo_name || '',
git_url: initialData?.git_config?.git_url || '',
git_owner: initialData?.git_config?.git_owner || 'whoosh',
git_branch: initialData?.git_config?.git_branch || 'main',
auto_initialize: initialData?.git_config?.auto_initialize ?? true,
private: initialData?.git_config?.private ?? false,
license_type: initialData?.git_config?.license_type || 'MIT'
},
bzzz_config: {
git_url: initialData?.bzzz_config?.git_url || '',
git_owner: initialData?.bzzz_config?.git_owner || '',
git_repository: initialData?.bzzz_config?.git_repository || '',
git_branch: initialData?.bzzz_config?.git_branch || 'main',
bzzz_enabled: initialData?.bzzz_config?.bzzz_enabled || false,
ready_to_claim: initialData?.bzzz_config?.ready_to_claim || false,
private_repo: initialData?.bzzz_config?.private_repo || false,
github_token_required: initialData?.bzzz_config?.github_token_required || false
enable_bzzz: initialData?.bzzz_config?.enable_bzzz ?? false,
task_coordination: initialData?.bzzz_config?.task_coordination ?? true,
ai_agent_access: initialData?.bzzz_config?.ai_agent_access ?? false,
auto_discovery: initialData?.bzzz_config?.auto_discovery ?? true
},
member_config: {
initial_members: initialData?.member_config?.initial_members || []
},
advanced_config: {
project_visibility: initialData?.advanced_config?.project_visibility || 'private',
security_level: initialData?.advanced_config?.security_level || 'standard',
backup_enabled: initialData?.advanced_config?.backup_enabled ?? true,
monitoring_enabled: initialData?.advanced_config?.monitoring_enabled ?? true
}
}
});
const currentTags = watch('tags') || [];
const gitUrl = watch('bzzz_config.git_url') || '';
const bzzzEnabled = watch('bzzz_config.bzzz_enabled') || false;
const selectedTemplate = watch('template_id') || '';
const repoType = watch('git_config.repo_type') || 'new';
const projectName = watch('name') || '';
const bzzzEnabled = watch('bzzz_config.enable_bzzz') || false;
const generateAgeKeys = watch('age_config.generate_new_key') ?? true;
// Auto-parse Git URL to extract owner and repository
const parseGitUrl = (url: string) => {
if (!url) return;
try {
// Handle GitHub URLs like https://github.com/owner/repo or git@github.com:owner/repo.git
const githubMatch = url.match(/github\.com[/:]([\w-]+)\/([\w-]+)(?:\.git)?$/);
if (githubMatch) {
const [, owner, repo] = githubMatch;
setValue('bzzz_config.git_owner', owner);
setValue('bzzz_config.git_repository', repo);
}
} catch (error) {
console.log('Could not parse Git URL:', error);
// Fetch available project templates
const { data: templates } = useQuery({
queryKey: ['project-templates'],
queryFn: async () => {
const response = await fetch('/api/project-setup/templates');
if (!response.ok) throw new Error('Failed to fetch templates');
return response.json();
}
});
// Auto-generate repository name from project name
useEffect(() => {
if (projectName && repoType === 'new') {
const repoName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
setValue('git_config.repo_name', repoName);
}
}, [projectName, repoType, setValue]);
// Setup step validation
const isStepValid = async (step: SetupStep): Promise<boolean> => {
switch (step) {
case 'basic':
return await trigger(['name', 'description']);
case 'git':
return await trigger(['git_config']);
case 'age-keys':
return await trigger(['age_config']);
default:
return true;
}
};
// Watch for Git URL changes and auto-parse
const handleGitUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const url = e.target.value;
parseGitUrl(url);
// Navigate between steps
const goToStep = async (step: SetupStep) => {
const valid = await isStepValid(currentStep);
if (valid || step === 'basic') {
setCurrentStep(step);
}
};
const nextStep = async () => {
const steps: SetupStep[] = ['basic', 'template', 'git', 'age-keys', 'bzzz', 'members', 'review'];
const currentIndex = steps.indexOf(currentStep);
if (currentIndex < steps.length - 1) {
await goToStep(steps[currentIndex + 1]);
}
};
const prevStep = () => {
const steps: SetupStep[] = ['basic', 'template', 'git', 'age-keys', 'bzzz', 'members', 'review'];
const currentIndex = steps.indexOf(currentStep);
if (currentIndex > 0) {
setCurrentStep(steps[currentIndex - 1]);
}
};
const createProjectMutation = useMutation({
mutationFn: async (data: ProjectFormData) => {
// In a real app, this would be an API call
const response = await fetch('/api/projects', {
mutationFn: async (data: ProjectSetupData) => {
setIsCreatingProject(true);
setSetupProgress([]);
const response = await fetch('/api/project-setup/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to create project');
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to create project');
}
return response.json();
},
onSuccess: (newProject) => {
onSuccess: (result) => {
setIsCreatingProject(false);
setSetupProgress(result.progress || []);
queryClient.invalidateQueries({ queryKey: ['projects'] });
toast.success('Project created successfully!');
navigate(`/projects/${newProject.id}`);
// Show completion with next steps
setTimeout(() => {
navigate(`/projects/${result.project_id}`);
}, 2000);
},
onError: (error) => {
toast.error('Failed to create project');
setIsCreatingProject(false);
toast.error(`Failed to create project: ${error.message}`);
console.error('Create project error:', error);
}
});
const updateProjectMutation = useMutation({
mutationFn: async (data: ProjectFormData) => {
mutationFn: async (data: ProjectSetupData) => {
const response = await fetch(`/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -147,8 +273,10 @@ export default function ProjectForm({ mode, initialData, projectId }: ProjectFor
}
});
const onSubmit = (data: ProjectFormData) => {
const onSubmit = (data: ProjectSetupData) => {
if (mode === 'create') {
// Set final step to show progress
setCurrentStep('review');
createProjectMutation.mutate(data);
} else {
updateProjectMutation.mutate(data);
@@ -303,6 +431,105 @@ export default function ProjectForm({ mode, initialData, projectId }: ProjectFor
</div>
</div>
{/* Age Encryption Configuration */}
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center space-x-2">
<h2 className="text-lg font-medium text-gray-900">🔐 Age Encryption Keys</h2>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Secure
</span>
</div>
<p className="text-sm text-gray-500 mt-1">
Generate master encryption keys for secure project data and member communication.
</p>
</div>
<div className="px-6 py-4 space-y-6">
{/* Generate Age Keys */}
<div>
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="generate_age_keys"
{...register('age_config.generate_new_key')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="generate_age_keys" className="text-sm font-medium text-gray-700">
Generate Age master key pair for this project
</label>
</div>
<p className="text-sm text-gray-500 mt-1 ml-7">
Creates secure encryption keys for project data, member communication, and sensitive information.
</p>
</div>
{/* Age Key Configuration - Only show if enabled */}
{generateAgeKeys && (
<div className="space-y-4 ml-7">
{/* Master Key Passphrase */}
<div>
<label htmlFor="master_key_passphrase" className="block text-sm font-medium text-gray-700 mb-2">
Master Key Passphrase (Optional)
</label>
<input
type="password"
id="master_key_passphrase"
{...register('age_config.master_key_passphrase')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter a strong passphrase for additional security"
/>
<p className="mt-1 text-sm text-gray-500">
Encrypts your private key with a passphrase. Leave empty for unencrypted storage.
</p>
</div>
{/* Backup Location */}
<div>
<label htmlFor="key_backup_location" className="block text-sm font-medium text-gray-700 mb-2">
Key Backup Location (Optional)
</label>
<input
type="text"
id="key_backup_location"
{...register('age_config.key_backup_location')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="/secure/backup/location or cloud storage path"
/>
<p className="mt-1 text-sm text-gray-500">
Automatically create a backup of your encryption keys at this location.
</p>
</div>
{/* Age Key Features Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-blue-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">
Age Encryption Features
</h3>
<div className="mt-2 text-sm text-blue-700">
<p>Your Age master keys will enable:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>End-to-end encryption of sensitive project data</li>
<li>Secure member-to-member communication</li>
<li>Encrypted project configuration and secrets</li>
<li>12-word recovery phrase generation</li>
<li>Automatic key backup and distribution</li>
</ul>
<p className="mt-2 font-medium">
Keys are stored securely with restricted file permissions.
</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
{/* Project Metadata */}
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
@@ -380,11 +607,11 @@ export default function ProjectForm({ mode, initialData, projectId }: ProjectFor
<input
type="checkbox"
id="bzzz_enabled"
{...register('bzzz_config.bzzz_enabled')}
{...register('bzzz_config.enable_bzzz')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="bzzz_enabled" className="text-sm font-medium text-gray-700">
Enable Bzzz P2P coordination for this project
Enable BZZZ P2P coordination for this project
</label>
</div>
<p className="text-sm text-gray-500 mt-1 ml-7">
@@ -392,145 +619,303 @@ export default function ProjectForm({ mode, initialData, projectId }: ProjectFor
</p>
</div>
{/* Git Repository Configuration - Only show if Bzzz is enabled */}
{bzzzEnabled && (
<>
{/* Git Repository URL */}
<div>
<label htmlFor="git_url" className="block text-sm font-medium text-gray-700 mb-2">
Git Repository URL *
</label>
<input
type="url"
id="git_url"
{...register('bzzz_config.git_url')}
onChange={handleGitUrlChange}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="https://github.com/owner/repository"
/>
<p className="mt-1 text-sm text-gray-500">
GitHub repository URL where Bzzz will look for issues labeled with 'bzzz-task'.
</p>
{errors.bzzz_config?.git_url && (
<p className="mt-1 text-sm text-red-600">{errors.bzzz_config.git_url.message}</p>
)}
</div>
{/* Git Repository Configuration */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-4">🔗 Git Repository Setup</h3>
{/* Repository Type */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Repository Type
</label>
<select
{...register('git_config.repo_type')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="new">Create new repository</option>
<option value="existing">Use existing repository</option>
</select>
</div>
{/* Auto-parsed Git Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="git_owner" className="block text-sm font-medium text-gray-700 mb-2">
Repository Owner
</label>
<input
type="text"
id="git_owner"
{...register('bzzz_config.git_owner')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Auto-detected from URL"
readOnly
/>
</div>
<div>
<label htmlFor="git_repository" className="block text-sm font-medium text-gray-700 mb-2">
Repository Name
</label>
<input
type="text"
id="git_repository"
{...register('bzzz_config.git_repository')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Auto-detected from URL"
readOnly
/>
</div>
</div>
{/* Git Branch */}
{/* Repository Name (auto-generated for new repos) */}
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="git_branch" className="block text-sm font-medium text-gray-700 mb-2">
Default Branch
<label htmlFor="git_owner" className="block text-sm font-medium text-gray-700 mb-2">
Repository Owner
</label>
<input
type="text"
id="git_branch"
{...register('bzzz_config.git_branch')}
id="git_owner"
{...register('git_config.git_owner')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="main"
placeholder="whoosh"
/>
</div>
<div>
<label htmlFor="git_repository" className="block text-sm font-medium text-gray-700 mb-2">
Repository Name
</label>
<input
type="text"
id="git_repository"
{...register('git_config.repo_name')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Auto-generated from project name"
readOnly={repoType === 'new'}
/>
</div>
</div>
{/* Repository Configuration */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">Repository Configuration</h3>
<div className="space-y-2">
{/* Ready to Claim */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="ready_to_claim"
{...register('bzzz_config.ready_to_claim')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="ready_to_claim" className="text-sm text-gray-700">
Ready for task claims (agents can start working immediately)
</label>
</div>
{/* Git Branch */}
<div className="mt-4">
<label htmlFor="git_branch" className="block text-sm font-medium text-gray-700 mb-2">
Default Branch
</label>
<input
type="text"
id="git_branch"
{...register('git_config.git_branch')}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="main"
/>
</div>
{/* Private Repository */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="private_repo"
{...register('bzzz_config.private_repo')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="private_repo" className="text-sm text-gray-700">
Private repository (requires authentication)
</label>
</div>
{/* Repository Privacy */}
<div className="mt-4">
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="private_repo"
{...register('git_config.private')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="private_repo" className="text-sm text-gray-700">
Private repository
</label>
</div>
</div>
</div>
{/* GitHub Token Required */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="github_token_required"
{...register('bzzz_config.github_token_required')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="github_token_required" className="text-sm text-gray-700">
Requires GitHub token for API access
</label>
</div>
{/* BZZZ Integration Features - Only show if enabled */}
{bzzzEnabled && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-700">BZZZ Task Coordination Features</h3>
<div className="space-y-2">
{/* Task Coordination */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="task_coordination"
{...register('bzzz_config.task_coordination')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="task_coordination" className="text-sm text-gray-700">
Enable automatic task coordination
</label>
</div>
{/* AI Agent Access */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="ai_agent_access"
{...register('bzzz_config.ai_agent_access')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="ai_agent_access" className="text-sm text-gray-700">
Allow AI agents to access and modify project files
</label>
</div>
{/* Auto Discovery */}
<div className="flex items-center space-x-3">
<input
type="checkbox"
id="auto_discovery"
{...register('bzzz_config.auto_discovery')}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="auto_discovery" className="text-sm text-gray-700">
Enable automatic peer discovery
</label>
</div>
</div>
{/* Bzzz Integration Info */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-yellow-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
How Bzzz Integration Works
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>When enabled, Bzzz agents will:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Monitor GitHub issues labeled with 'bzzz-task'</li>
<li>Coordinate P2P to assign tasks based on agent capabilities</li>
<li>Execute tasks using distributed AI reasoning</li>
<li>Report progress and escalate when needed</li>
</ul>
<p className="mt-2 font-medium">
Make sure your repository has issues labeled with 'bzzz-task' for agents to discover.
</p>
</div>
</div>
</div>
</div>
</>
</div>
)}
{/* BZZZ Integration Info */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-yellow-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
How BZZZ Integration Works
</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>When enabled, BZZZ agents will:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Monitor GITEA issues labeled with 'bzzz-task'</li>
<li>Coordinate P2P to assign tasks based on agent capabilities</li>
<li>Execute tasks using distributed AI reasoning</li>
<li>Report progress and escalate when needed</li>
</ul>
<p className="mt-2 font-medium">
A GITEA repository will be automatically created with proper BZZZ labels configured.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Team Members Configuration */}
<div className="bg-white shadow-sm rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center space-x-2">
<h2 className="text-lg font-medium text-gray-900">👥 Team Members</h2>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Optional
</span>
</div>
<p className="text-sm text-gray-500 mt-1">
Invite team members to collaborate on this project from the start.
</p>
</div>
<div className="px-6 py-4 space-y-6">
{/* Member Invitations */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Initial Team Members
</label>
{/* Current Members */}
{watch('member_config.initial_members')?.length > 0 && (
<div className="space-y-2 mb-4">
{watch('member_config.initial_members').map((member, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div className="flex-1">
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-gray-900">{member.email}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
member.role === 'owner' ? 'bg-purple-100 text-purple-800' :
member.role === 'maintainer' ? 'bg-blue-100 text-blue-800' :
member.role === 'developer' ? 'bg-green-100 text-green-800' :
'bg-gray-100 text-gray-800'
}`}>
{member.role.charAt(0).toUpperCase() + member.role.slice(1)}
</span>
</div>
</div>
<button
type="button"
onClick={() => {
const currentMembers = watch('member_config.initial_members') || [];
const updatedMembers = currentMembers.filter((_, i) => i !== index);
setValue('member_config.initial_members', updatedMembers);
}}
className="text-red-600 hover:text-red-800"
>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
{/* Add Member Form */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<input
type="email"
placeholder="team.member@company.com"
className="block w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const emailInput = e.target as HTMLInputElement;
const roleSelect = emailInput.parentElement?.nextElementSibling?.querySelector('select') as HTMLSelectElement;
if (emailInput.value && roleSelect?.value) {
const currentMembers = watch('member_config.initial_members') || [];
const newMember = {
email: emailInput.value,
role: roleSelect.value as 'owner' | 'maintainer' | 'developer' | 'viewer'
};
setValue('member_config.initial_members', [...currentMembers, newMember]);
emailInput.value = '';
roleSelect.value = 'developer';
}
}
}}
/>
</div>
<div>
<select
className="block w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
defaultValue="developer"
>
<option value="developer">Developer</option>
<option value="maintainer">Maintainer</option>
<option value="viewer">Viewer</option>
<option value="owner">Owner</option>
</select>
</div>
<div>
<button
type="button"
onClick={() => {
const container = document.querySelector('.border-dashed');
const emailInput = container?.querySelector('input[type="email"]') as HTMLInputElement;
const roleSelect = container?.querySelector('select') as HTMLSelectElement;
if (emailInput?.value && roleSelect?.value) {
const currentMembers = watch('member_config.initial_members') || [];
const newMember = {
email: emailInput.value,
role: roleSelect.value as 'owner' | 'maintainer' | 'developer' | 'viewer'
};
setValue('member_config.initial_members', [...currentMembers, newMember]);
emailInput.value = '';
roleSelect.value = 'developer';
}
}}
className="w-full inline-flex items-center justify-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<PlusIcon className="h-4 w-4 mr-1" />
Add Member
</button>
</div>
</div>
<p className="mt-2 text-xs text-gray-500">
Press Enter in the email field or click "Add Member" to add team members
</p>
</div>
</div>
{/* Member Features Info */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-green-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800">
Team Member Features
</h3>
<div className="mt-2 text-sm text-green-700">
<p>Team members will receive:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Email invitation with project details and role information</li>
<li>Access to GITEA repository based on their role</li>
<li>Age encryption keys for secure project communication</li>
<li>Role-based permissions for project management</li>
<li>Integration with BZZZ task coordination system</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -21,7 +21,7 @@ import { projectApi } from '../../services/api';
export default function ProjectList() {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'archived'>('all');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'arcwhooshd'>('all');
const [bzzzFilter, setBzzzFilter] = useState<'all' | 'enabled' | 'disabled'>('all');
// Fetch real projects from API
@@ -52,7 +52,7 @@ export default function ProjectList() {
return `${baseClasses} bg-green-100 text-green-800`;
case 'inactive':
return `${baseClasses} bg-gray-100 text-gray-800`;
case 'archived':
case 'arcwhooshd':
return `${baseClasses} bg-red-100 text-red-800`;
default:
return `${baseClasses} bg-gray-100 text-gray-800`;
@@ -138,7 +138,7 @@ export default function ProjectList() {
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="archived">Archived</option>
<option value="arcwhooshd">Arcwhooshd</option>
</select>
</div>
@@ -248,10 +248,10 @@ export default function ProjectList() {
<button
className={`${active ? 'bg-gray-100' : ''} block w-full text-left px-4 py-2 text-sm text-red-700`}
onClick={() => {
// Handle archive/delete
// Handle arcwhoosh/delete
}}
>
Archive Project
Arcwhoosh Project
</button>
)}
</Menu.Item>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
/**
* Cluster Detector Component
* Detects if cluster exists and routes to setup wizard or main app accordingly
*/
import React, { useState, useEffect } from 'react';
import { Loader2 } from 'lucide-react';
import SetupWizard from './SetupWizard';
import { apiConfig } from '../../config/api';
import { useTheme } from '../../contexts/ThemeContext';
interface ClusterDetectorProps {
children: React.ReactNode;
}
export const ClusterDetector: React.FC<ClusterDetectorProps> = ({ children }) => {
const { isDarkMode } = useTheme();
const [loading, setLoading] = useState(true);
const [clusterExists, setClusterExists] = useState(false);
const [error, setError] = useState<string>('');
const API_BASE_URL = apiConfig.baseURL + '/api';
useEffect(() => {
checkClusterStatus();
}, []);
const checkClusterStatus = async () => {
try {
setLoading(true);
setError('');
const response = await fetch(`${API_BASE_URL}/cluster-setup/status`);
const data = await response.json();
if (data.success) {
// Check if cluster is fully initialized
setClusterExists(data.data.cluster_initialized || false);
} else {
// If we can't check status, assume no cluster exists
setClusterExists(false);
}
} catch (err: any) {
console.error('Error checking cluster status:', err);
// On error, assume no cluster exists to trigger setup
setClusterExists(false);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className={`min-h-screen flex items-center justify-center ${
isDarkMode
? 'bg-gradient-to-br from-gray-900 to-gray-800'
: 'bg-gradient-to-br from-blue-50 to-indigo-100'
}`}>
<div className="text-center">
<Loader2 className={`h-8 w-8 animate-spin mx-auto ${
isDarkMode ? 'text-blue-400' : 'text-blue-600'
}`} />
<p className={`mt-2 ${
isDarkMode ? 'text-gray-300' : 'text-gray-600'
}`}>Detecting cluster status...</p>
</div>
</div>
);
}
if (error) {
return (
<div className={`min-h-screen flex items-center justify-center ${
isDarkMode
? 'bg-gradient-to-br from-red-900/20 to-red-800/20'
: 'bg-gradient-to-br from-red-50 to-red-100'
}`}>
<div className="text-center">
<div className={`mb-4 ${
isDarkMode ? 'text-red-400' : 'text-red-600'
}`}>
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 19c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h1 className={`text-2xl font-bold mb-2 ${
isDarkMode ? 'text-red-300' : 'text-red-900'
}`}>Cluster Detection Error</h1>
<p className={`mb-4 ${
isDarkMode ? 'text-red-400' : 'text-red-700'
}`}>{error}</p>
<button
onClick={checkClusterStatus}
className={`px-4 py-2 rounded ${
isDarkMode
? 'bg-red-600 text-white hover:bg-red-700'
: 'bg-red-600 text-white hover:bg-red-700'
}`}
>
Retry
</button>
</div>
</div>
);
}
// If cluster doesn't exist, show setup wizard
if (!clusterExists) {
return <SetupWizard />;
}
// If cluster exists, show the main app
return <>{children}</>;
};
export default ClusterDetector;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,421 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import {
MagnifyingGlassIcon,
FunnelIcon,
ClockIcon,
TagIcon,
UserIcon,
CodeBracketIcon,
DocumentTextIcon,
ChevronRightIcon,
StarIcon,
DownloadIcon,
EyeIcon
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
interface Template {
template_id: string;
name: string;
description: string;
icon: string;
category: string;
tags: string[];
difficulty: string;
estimated_setup_time: string;
features: string[];
tech_stack: Record<string, string[]>;
requirements?: Record<string, string>;
}
interface TemplateBrowserProps {
onSelectTemplate?: (template: Template) => void;
onCreateProject?: (templateId: string) => void;
mode?: 'browse' | 'select';
}
export default function TemplateBrowser({
onSelectTemplate,
onCreateProject,
mode = 'browse'
}: TemplateBrowserProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [selectedDifficulty, setSelectedDifficulty] = useState<string>('all');
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [showPreview, setShowPreview] = useState(false);
// Fetch templates
const { data: templatesResponse, isLoading, error } = useQuery({
queryKey: ['templates', selectedCategory, selectedDifficulty],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedCategory !== 'all') params.append('category', selectedCategory);
if (selectedDifficulty !== 'all') params.append('difficulty', selectedDifficulty);
const response = await fetch(`/api/templates?${params}`);
if (!response.ok) throw new Error('Failed to fetch templates');
return response.json();
}
});
// Create project from template mutation
const createProjectMutation = useMutation({
mutationFn: async (templateId: string) => {
const response = await fetch('/api/templates/create-project', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
template_id: templateId,
project_name: `${selectedTemplate?.name.replace(/\s+/g, '-').toLowerCase()}-project`,
create_repository: true
})
});
if (!response.ok) throw new Error('Failed to create project');
return response.json();
},
onSuccess: (result) => {
toast.success(`Project created successfully!`);
if (onCreateProject) {
onCreateProject(result.project_id);
}
},
onError: (error) => {
toast.error(`Failed to create project: ${error.message}`);
}
});
const templates = templatesResponse?.templates || [];
const categories = templatesResponse?.categories || [];
// Filter templates based on search query
const filteredTemplates = templates.filter((template: Template) => {
const matchesSearch = searchQuery === '' ||
template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
return matchesSearch;
});
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'bg-green-100 text-green-800';
case 'intermediate': return 'bg-yellow-100 text-yellow-800';
case 'advanced': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getCategoryIcon = (category: string) => {
switch (category) {
case 'web-development': return '🌐';
case 'data-science': return '📊';
case 'mobile': return '📱';
case 'devops': return '🔧';
case 'ai-ml': return '🤖';
default: return '📁';
}
};
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template);
if (onSelectTemplate) {
onSelectTemplate(template);
}
};
const handleCreateProject = (templateId: string) => {
createProjectMutation.mutate(templateId);
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-gray-200 rounded-lg h-32"></div>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="p-6 text-center text-red-600">
Failed to load templates: {error.message}
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-sm">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-medium text-gray-900">Project Templates</h2>
<p className="text-sm text-gray-500">
Choose from {templates.length} professionally crafted project templates
</p>
</div>
{mode === 'browse' && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">
{filteredTemplates.length} templates
</span>
</div>
)}
</div>
</div>
{/* Filters and Search */}
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="flex-1">
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search templates..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* Category Filter */}
<div className="sm:w-48">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Categories</option>
{categories.map((category) => (
<option key={category} value={category}>
{category.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</option>
))}
</select>
</div>
{/* Difficulty Filter */}
<div className="sm:w-36">
<select
value={selectedDifficulty}
onChange={(e) => setSelectedDifficulty(e.target.value)}
className="block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">All Levels</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
</div>
</div>
{/* Template Grid */}
<div className="p-6">
{filteredTemplates.length === 0 ? (
<div className="text-center py-12">
<DocumentTextIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No templates found</h3>
<p className="mt-1 text-sm text-gray-500">
Try adjusting your search or filter criteria.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredTemplates.map((template: Template) => (
<div
key={template.template_id}
className={`border rounded-lg p-6 hover:shadow-md transition-shadow cursor-pointer ${
selectedTemplate?.template_id === template.template_id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => handleTemplateSelect(template)}
>
{/* Template Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<span className="text-2xl">{template.icon}</span>
<div>
<h3 className="text-lg font-medium text-gray-900">{template.name}</h3>
<div className="flex items-center space-x-2 mt-1">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getDifficultyColor(template.difficulty)}`}>
{template.difficulty}
</span>
<span className="text-xs text-gray-500 flex items-center">
<ClockIcon className="h-3 w-3 mr-1" />
{template.estimated_setup_time}
</span>
</div>
</div>
</div>
</div>
{/* Description */}
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
{/* Tech Stack */}
{Object.keys(template.tech_stack).length > 0 && (
<div className="mb-4">
<p className="text-xs font-medium text-gray-700 mb-2">Tech Stack:</p>
<div className="flex flex-wrap gap-1">
{Object.entries(template.tech_stack).slice(0, 3).map(([category, techs]) => (
<div key={category} className="flex flex-wrap gap-1">
{techs.slice(0, 2).map((tech) => (
<span
key={tech}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-800"
>
{tech}
</span>
))}
</div>
))}
{Object.values(template.tech_stack).flat().length > 6 && (
<span className="text-xs text-gray-500">+more</span>
)}
</div>
</div>
)}
{/* Tags */}
{template.tags.length > 0 && (
<div className="mb-4">
<div className="flex flex-wrap gap-1">
{template.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-100 text-blue-800"
>
<TagIcon className="h-3 w-3 mr-1" />
{tag}
</span>
))}
{template.tags.length > 3 && (
<span className="text-xs text-gray-500">+{template.tags.length - 3} more</span>
)}
</div>
</div>
)}
{/* Features */}
<div className="mb-4">
<p className="text-xs font-medium text-gray-700 mb-2">Key Features:</p>
<ul className="text-xs text-gray-600 space-y-1">
{template.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-center">
<ChevronRightIcon className="h-3 w-3 mr-1 text-gray-400" />
{feature}
</li>
))}
{template.features.length > 3 && (
<li className="text-gray-500">... and {template.features.length - 3} more features</li>
)}
</ul>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div className="flex items-center space-x-2">
<button
onClick={(e) => {
e.stopPropagation();
setShowPreview(true);
}}
className="text-sm text-gray-600 hover:text-gray-800 flex items-center"
>
<EyeIcon className="h-4 w-4 mr-1" />
Preview
</button>
</div>
{mode === 'browse' && (
<button
onClick={(e) => {
e.stopPropagation();
handleCreateProject(template.template_id);
}}
disabled={createProjectMutation.isLoading}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{createProjectMutation.isLoading ? (
<>
<div className="animate-spin -ml-1 mr-2 h-3 w-3 border border-white border-t-transparent rounded-full"></div>
Creating...
</>
) : (
<>
<CodeBracketIcon className="h-4 w-4 mr-1" />
Use Template
</>
)}
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Template Details Modal */}
{selectedTemplate && mode === 'select' && (
<div className="mt-6 p-6 border-t border-gray-200 bg-gray-50">
<h3 className="text-lg font-medium text-gray-900 mb-4">
{selectedTemplate.name} Details
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-gray-900 mb-2">Features</h4>
<ul className="text-sm text-gray-600 space-y-1">
{selectedTemplate.features.map((feature, index) => (
<li key={index} className="flex items-center">
<ChevronRightIcon className="h-4 w-4 mr-2 text-gray-400" />
{feature}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-2">Technology Stack</h4>
<div className="space-y-2">
{Object.entries(selectedTemplate.tech_stack).map(([category, techs]) => (
<div key={category}>
<p className="text-xs font-medium text-gray-700 capitalize">
{category.replace('_', ' ')}:
</p>
<div className="flex flex-wrap gap-1 mt-1">
{techs.map((tech) => (
<span
key={tech}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800"
>
{tech}
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,277 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
CheckCircleIcon,
ChevronRightIcon,
ClockIcon,
TagIcon,
InformationCircleIcon
} from '@heroicons/react/24/outline';
interface Template {
template_id: string;
name: string;
description: string;
icon: string;
category: string;
tags: string[];
difficulty: string;
estimated_setup_time: string;
features: string[];
tech_stack: Record<string, string[]>;
}
interface TemplateSelectorProps {
selectedTemplateId?: string;
onSelectTemplate: (templateId: string | null) => void;
showDetails?: boolean;
}
export default function TemplateSelector({
selectedTemplateId,
onSelectTemplate,
showDetails = true
}: TemplateSelectorProps) {
const [expandedTemplate, setExpandedTemplate] = useState<string | null>(null);
// Fetch templates
const { data: templatesResponse, isLoading } = useQuery({
queryKey: ['templates'],
queryFn: async () => {
const response = await fetch('/api/templates');
if (!response.ok) throw new Error('Failed to fetch templates');
return response.json();
}
});
const templates = templatesResponse?.templates || [];
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'beginner': return 'bg-green-100 text-green-800';
case 'intermediate': return 'bg-yellow-100 text-yellow-800';
case 'advanced': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const selectedTemplate = templates.find((t: Template) => t.template_id === selectedTemplateId);
if (isLoading) {
return (
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse bg-gray-200 rounded-lg h-16"></div>
))}
</div>
);
}
return (
<div className="space-y-4">
{/* No Template Option */}
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
selectedTemplateId === null
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => onSelectTemplate(null)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedTemplateId === null
? 'border-blue-500 bg-blue-500'
: 'border-gray-300'
}`}>
{selectedTemplateId === null && (
<CheckCircleIcon className="w-3 h-3 text-white" />
)}
</div>
<div>
<h3 className="font-medium text-gray-900">Start from Scratch</h3>
<p className="text-sm text-gray-500">Create an empty project with basic structure</p>
</div>
</div>
<span className="text-2xl">📁</span>
</div>
</div>
{/* Template Options */}
{templates.map((template: Template) => (
<div key={template.template_id}>
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
selectedTemplateId === template.template_id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => onSelectTemplate(template.template_id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedTemplateId === template.template_id
? 'border-blue-500 bg-blue-500'
: 'border-gray-300'
}`}>
{selectedTemplateId === template.template_id && (
<CheckCircleIcon className="w-3 h-3 text-white" />
)}
</div>
<div className="flex-1">
<div className="flex items-center space-x-2">
<h3 className="font-medium text-gray-900">{template.name}</h3>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getDifficultyColor(template.difficulty)}`}>
{template.difficulty}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">{template.description}</p>
{/* Quick info */}
<div className="flex items-center space-x-4 mt-2">
<span className="text-xs text-gray-500 flex items-center">
<ClockIcon className="h-3 w-3 mr-1" />
{template.estimated_setup_time}
</span>
<span className="text-xs text-gray-500">
{template.features.length} features
</span>
{template.tags.length > 0 && (
<div className="flex items-center space-x-1">
<TagIcon className="h-3 w-3 text-gray-400" />
<span className="text-xs text-gray-500">
{template.tags.slice(0, 2).join(', ')}
{template.tags.length > 2 && ` +${template.tags.length - 2}`}
</span>
</div>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<span className="text-2xl">{template.icon}</span>
{showDetails && (
<button
onClick={(e) => {
e.stopPropagation();
setExpandedTemplate(
expandedTemplate === template.template_id ? null : template.template_id
);
}}
className="p-1 text-gray-400 hover:text-gray-600"
>
<ChevronRightIcon
className={`h-4 w-4 transition-transform ${
expandedTemplate === template.template_id ? 'rotate-90' : ''
}`}
/>
</button>
)}
</div>
</div>
</div>
{/* Expanded Details */}
{showDetails && expandedTemplate === template.template_id && (
<div className="mt-2 ml-8 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Features */}
<div>
<h4 className="font-medium text-gray-900 mb-2">Features</h4>
<ul className="text-sm text-gray-600 space-y-1">
{template.features.slice(0, 6).map((feature, index) => (
<li key={index} className="flex items-start">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
{feature}
</li>
))}
{template.features.length > 6 && (
<li className="text-gray-500 text-xs">
... and {template.features.length - 6} more features
</li>
)}
</ul>
</div>
{/* Tech Stack */}
<div>
<h4 className="font-medium text-gray-900 mb-2">Technology Stack</h4>
<div className="space-y-2">
{Object.entries(template.tech_stack).slice(0, 4).map(([category, techs]) => (
<div key={category}>
<p className="text-xs font-medium text-gray-700 capitalize mb-1">
{category.replace('_', ' ')}:
</p>
<div className="flex flex-wrap gap-1">
{techs.slice(0, 4).map((tech) => (
<span
key={tech}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800"
>
{tech}
</span>
))}
{techs.length > 4 && (
<span className="text-xs text-gray-500">+{techs.length - 4} more</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
{/* Requirements */}
{template.requirements && Object.keys(template.requirements).length > 0 && (
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<div className="flex">
<InformationCircleIcon className="h-5 w-5 text-yellow-400" />
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Requirements</h3>
<div className="mt-1 text-sm text-yellow-700">
<div className="flex flex-wrap gap-2">
{Object.entries(template.requirements).map(([req, version]) => (
<span
key={req}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-yellow-100 text-yellow-800"
>
{req} {version}
</span>
))}
</div>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
))}
{/* Selected Template Summary */}
{selectedTemplate && (
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start space-x-3">
<CheckCircleIcon className="h-5 w-5 text-blue-500 mt-0.5" />
<div>
<h3 className="text-sm font-medium text-blue-900">
Template Selected: {selectedTemplate.name}
</h3>
<p className="text-sm text-blue-700 mt-1">
Your project will be created with {selectedTemplate.features.length} pre-configured features
and {Object.values(selectedTemplate.tech_stack).flat().length} technology integrations.
</p>
<p className="text-xs text-blue-600 mt-2">
Estimated setup time: {selectedTemplate.estimated_setup_time}
</p>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -4,7 +4,7 @@ import { Badge } from './badge';
import { Button } from './button';
/**
* DataTable component for Hive UI
* DataTable component for WHOOSH UI
*
* A powerful and flexible data table component with sorting, filtering, searching, and pagination.
* Perfect for displaying agent lists, task queues, and workflow executions.
@@ -17,7 +17,7 @@ const meta = {
docs: {
description: {
component: `
The DataTable component is a comprehensive solution for displaying tabular data in the Hive application.
The DataTable component is a comprehensive solution for displaying tabular data in the WHOOSH application.
It provides powerful features for data manipulation and user interaction.
## Features

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Badge } from './badge';
/**
* Badge component for Hive UI
* Badge component for WHOOSH UI
*
* A small status indicator component used to display labels, statuses, and categories.
* Perfect for showing agent statuses, task priorities, and workflow states.
@@ -15,7 +15,7 @@ const meta = {
docs: {
description: {
component: `
The Badge component is used to display small labels and status indicators throughout the Hive application.
The Badge component is used to display small labels and status indicators throughout the WHOOSH application.
It's commonly used for showing agent statuses, task priorities, and other categorical information.
## Features
@@ -149,7 +149,7 @@ export const AllVariants: Story = {
};
/**
* Agent status badges as used in Hive
* Agent status badges as used in WHOOSH
*/
export const AgentStatuses: Story = {
render: () => (
@@ -164,7 +164,7 @@ export const AgentStatuses: Story = {
parameters: {
docs: {
description: {
story: 'Common agent status badges used throughout the Hive application',
story: 'Common agent status badges used throughout the WHOOSH application',
},
},
},

View File

@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';
/**
* Button component for Hive UI
* Button component for WHOOSH UI
*
* A versatile button component with multiple variants, sizes, and states.
* Supports all standard button functionality with consistent styling.
@@ -15,7 +15,7 @@ const meta = {
docs: {
description: {
component: `
The Button component is a fundamental UI element used throughout the Hive application.
The Button component is a fundamental UI element used throughout the WHOOSH application.
It provides consistent styling and behavior across different contexts.
## Features
@@ -208,9 +208,9 @@ export const AllSizes: Story = {
};
/**
* Common Hive use cases
* Common WHOOSH use cases
*/
export const HiveUseCases: Story = {
export const WHOOSHUseCases: Story = {
render: () => (
<div className="flex flex-col gap-4 max-w-md">
<div className="flex gap-2">
@@ -234,7 +234,7 @@ export const HiveUseCases: Story = {
parameters: {
docs: {
description: {
story: 'Common button combinations used throughout the Hive application',
story: 'Common button combinations used throughout the WHOOSH application',
},
},
},

View File

@@ -4,7 +4,7 @@ import { Button } from './button';
import { Badge } from './badge';
/**
* Card component system for Hive UI
* Card component system for WHOOSH UI
*
* A flexible card component system that provides a container for content.
* Includes header, title, description, and content sections.
@@ -101,7 +101,7 @@ export const ContentOnly: Story = {
};
/**
* Agent status card as used in Hive
* Agent status card as used in WHOOSH
*/
export const AgentStatusCard: Story = {
render: () => (
@@ -140,14 +140,14 @@ export const AgentStatusCard: Story = {
parameters: {
docs: {
description: {
story: 'Example of how cards are used to display agent information in the Hive dashboard',
story: 'Example of how cards are used to display agent information in the WHOOSH dashboard',
},
},
},
};
/**
* Task execution card as used in Hive
* Task execution card as used in WHOOSH
*/
export const TaskCard: Story = {
render: () => (
@@ -189,14 +189,14 @@ export const TaskCard: Story = {
parameters: {
docs: {
description: {
story: 'Example of how cards are used to display task information in the Hive dashboard',
story: 'Example of how cards are used to display task information in the WHOOSH dashboard',
},
},
},
};
/**
* Workflow card as used in Hive
* Workflow card as used in WHOOSH
*/
export const WorkflowCard: Story = {
render: () => (
@@ -239,7 +239,7 @@ export const WorkflowCard: Story = {
parameters: {
docs: {
description: {
story: 'Example of how cards are used to display workflow information in the Hive dashboard',
story: 'Example of how cards are used to display workflow information in the WHOOSH dashboard',
},
},
},

View File

@@ -3,10 +3,14 @@ import React from 'react';
interface CardProps {
className?: string;
children: React.ReactNode;
onClick?: () => void;
}
export const Card: React.FC<CardProps> = ({ className = '', children }) => (
<div className={`bg-white rounded-lg shadow-md border ${className}`}>
export const Card: React.FC<CardProps> = ({ className = '', children, onClick }) => (
<div
className={`bg-white rounded-lg shadow-md border ${className}`}
onClick={onClick}
>
{children}
</div>
);

View File

@@ -4,9 +4,9 @@ import { Label } from './label';
import { Button } from './button';
/**
* Input component for Hive UI
* Input component for WHOOSH UI
*
* A versatile input component for forms and user input throughout the Hive application.
* A versatile input component for forms and user input throughout the WHOOSH application.
* Supports various input types with consistent styling and behavior.
*/
const meta = {
@@ -17,7 +17,7 @@ const meta = {
docs: {
description: {
component: `
The Input component provides consistent styling and behavior for form inputs across the Hive application.
The Input component provides consistent styling and behavior for form inputs across the WHOOSH application.
It supports all standard HTML input types with enhanced styling and focus states.
## Features

View File

@@ -7,7 +7,7 @@
export const getApiBaseUrl = (): string => {
// Production environment
if (import.meta.env.PROD) {
return import.meta.env.VITE_API_BASE_URL || 'https://hive.home.deepblack.cloud';
return import.meta.env.VITE_API_BASE_URL || 'https://whoosh.home.deepblack.cloud';
}
// Development environment - prefer environment variable
@@ -20,8 +20,8 @@ export const getApiBaseUrl = (): string => {
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 (hostname === 'whoosh.home.deepblack.cloud') {
return 'https://whoosh.home.deepblack.cloud';
}
// If we're on localhost, try to detect the backend port
@@ -38,7 +38,7 @@ export const getApiBaseUrl = (): string => {
export const getWebSocketUrl = (): string => {
// Production environment
if (import.meta.env.PROD) {
return import.meta.env.VITE_WS_BASE_URL || 'https://hive.home.deepblack.cloud';
return import.meta.env.VITE_WS_BASE_URL || 'https://whoosh.home.deepblack.cloud';
}
// Development environment
@@ -50,8 +50,8 @@ export const getWebSocketUrl = (): string => {
// 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 === 'whoosh.home.deepblack.cloud') {
return 'https://whoosh.home.deepblack.cloud';
}
if (hostname === 'localhost' || hostname === '127.0.0.1') {

View File

@@ -65,8 +65,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
useEffect(() => {
const initializeAuth = async () => {
try {
const storedTokens = localStorage.getItem('hive_tokens');
const storedUser = localStorage.getItem('hive_user');
const storedTokens = localStorage.getItem('whoosh_tokens');
const storedUser = localStorage.getItem('whoosh_user');
if (storedTokens && storedUser) {
const parsedTokens: AuthTokens = JSON.parse(storedTokens);
@@ -132,8 +132,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setTokens(newTokens);
setUser(data.user);
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
localStorage.setItem('hive_user', JSON.stringify(data.user));
localStorage.setItem('whoosh_tokens', JSON.stringify(newTokens));
localStorage.setItem('whoosh_user', JSON.stringify(data.user));
localStorage.setItem('token', newTokens.access_token);
return true;
@@ -174,8 +174,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setUser(data.user);
// Store in localStorage
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
localStorage.setItem('hive_user', JSON.stringify(data.user));
localStorage.setItem('whoosh_tokens', JSON.stringify(newTokens));
localStorage.setItem('whoosh_user', JSON.stringify(data.user));
localStorage.setItem('token', newTokens.access_token);
} catch (error: any) {
@@ -210,8 +210,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setUser(data.user);
// Store in localStorage
localStorage.setItem('hive_tokens', JSON.stringify(newTokens));
localStorage.setItem('hive_user', JSON.stringify(data.user));
localStorage.setItem('whoosh_tokens', JSON.stringify(newTokens));
localStorage.setItem('whoosh_user', JSON.stringify(data.user));
localStorage.setItem('token', newTokens.access_token);
} catch (error) {
console.error('Registration failed:', error);
@@ -249,15 +249,15 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
if (user) {
const updatedUser = { ...user, ...userData };
setUser(updatedUser);
localStorage.setItem('hive_user', JSON.stringify(updatedUser));
localStorage.setItem('whoosh_user', JSON.stringify(updatedUser));
}
};
const clearAuthData = (): void => {
setUser(null);
setTokens(null);
localStorage.removeItem('hive_tokens');
localStorage.removeItem('hive_user');
localStorage.removeItem('whoosh_tokens');
localStorage.removeItem('whoosh_user');
localStorage.removeItem('token');
};

View File

@@ -22,7 +22,7 @@ interface SocketIOProviderProps {
export const SocketIOProvider: React.FC<SocketIOProviderProps> = ({
children,
url = import.meta.env.VITE_WS_BASE_URL || 'https://hive.home.deepblack.cloud'
url = import.meta.env.VITE_WS_BASE_URL || 'https://whoosh.home.deepblack.cloud'
}) => {
// Allow disabling SocketIO completely via environment variable
const socketIODisabled = import.meta.env.VITE_DISABLE_SOCKETIO === 'true';
@@ -75,7 +75,7 @@ export const SocketIOProvider: React.FC<SocketIOProviderProps> = ({
}
},
onConnect: () => {
console.log('✅ Socket.IO connected to Hive backend');
console.log('✅ Socket.IO connected to WHOOSH backend');
// Join general room and subscribe to common events
if (socket) {
@@ -87,7 +87,7 @@ export const SocketIOProvider: React.FC<SocketIOProviderProps> = ({
}
},
onDisconnect: () => {
console.log('🔌 Socket.IO disconnected from Hive backend');
console.log('🔌 Socket.IO disconnected from WHOOSH backend');
},
onError: (error) => {
// Errors are already logged in the hook, don't duplicate

View File

@@ -27,8 +27,8 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
if (saved !== null) {
return JSON.parse(saved);
}
// Default to system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches;
// Default to dark mode as requested by user
return true;
});
useEffect(() => {

View File

@@ -19,7 +19,7 @@ interface WebSocketProviderProps {
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
children,
url = import.meta.env.VITE_WS_BASE_URL || 'wss://hive.home.deepblack.cloud'
url = import.meta.env.VITE_WS_BASE_URL || 'wss://whoosh.home.deepblack.cloud'
}) => {
const [subscriptions, setSubscriptions] = useState<Map<string, Set<(data: any) => void>>>(new Map());
@@ -47,14 +47,14 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
}
},
onConnect: () => {
console.log('WebSocket connected to Hive backend');
console.log('WebSocket connected to WHOOSH backend');
// Subscribe to general system events
sendMessage('subscribe', {
events: ['agent_status_changed', 'execution_started', 'execution_completed', 'metrics_updated']
});
},
onDisconnect: () => {
console.log('WebSocket disconnected from Hive backend');
console.log('WebSocket disconnected from WHOOSH backend');
},
onError: (error) => {
console.error('WebSocket error:', error);

View File

@@ -24,6 +24,48 @@
background-color: #0f172a;
}
/* Ensure proper text contrast in dark mode */
.dark {
color-scheme: dark;
}
.dark * {
border-color: rgb(55 65 81 / 0.4);
}
.dark input,
.dark textarea,
.dark select {
background-color: rgb(31 41 55);
color: rgb(243 244 246);
border-color: rgb(75 85 99);
}
.dark input::placeholder,
.dark textarea::placeholder {
color: rgb(156 163 175);
}
.dark .bg-white {
background-color: rgb(31 41 55) !important;
}
.dark .text-gray-900 {
color: rgb(243 244 246) !important;
}
.dark .text-gray-700 {
color: rgb(209 213 219) !important;
}
.dark .text-gray-600 {
color: rgb(156 163 175) !important;
}
.dark .text-gray-500 {
color: rgb(107 114 128) !important;
}
body {
margin: 0;
min-width: 320px;

View File

@@ -0,0 +1,581 @@
/**
* WHOOSH AI Models Management - Phase 6.1
* Advanced AI model integration interface
*/
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Textarea } from '../components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
import { Alert, AlertDescription } from '../components/ui/alert';
import {
Activity,
Brain,
Code,
Cpu,
FileText,
Gauge,
GitBranch,
MessageSquare,
RefreshCw,
Search,
Settings,
Zap
} from 'lucide-react';
interface AIModel {
name: string;
node_url: string;
capabilities: string[];
context_length: number;
parameter_count: string;
specialization?: string;
performance_score: number;
availability: boolean;
usage_count: number;
avg_response_time: number;
}
interface ClusterStatus {
total_nodes: number;
healthy_nodes: number;
total_models: number;
models_by_capability: Record<string, number>;
cluster_load: number;
model_usage_stats: Record<string, any>;
}
interface CompletionResponse {
success: boolean;
content?: string;
model: string;
response_time?: number;
error?: string;
}
const AIModels: React.FC = () => {
const [models, setModels] = useState<AIModel[]>([]);
const [clusterStatus, setClusterStatus] = useState<ClusterStatus | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCapability, setSelectedCapability] = useState<string>('all');
// AI Interaction State
const [prompt, setPrompt] = useState('');
const [systemPrompt, setSystemPrompt] = useState('');
const [selectedModel, setSelectedModel] = useState<string>('');
const [taskType, setTaskType] = useState<string>('');
const [response, setResponse] = useState<CompletionResponse | null>(null);
const [generating, setGenerating] = useState(false);
// Code Generation State
const [codeDescription, setCodeDescription] = useState('');
const [codeLanguage, setCodeLanguage] = useState('python');
const [codeStyle, setCodeStyle] = useState('clean');
const [codeResponse, setCodeResponse] = useState<CompletionResponse | null>(null);
const capabilityIcons: Record<string, any> = {
code_generation: Code,
code_review: GitBranch,
documentation: FileText,
testing: Search,
architecture: Settings,
debugging: Cpu,
refactoring: RefreshCw,
general_chat: MessageSquare,
specialized_domain: Brain
};
const capabilityColors: Record<string, string> = {
code_generation: 'bg-blue-100 text-blue-800',
code_review: 'bg-green-100 text-green-800',
documentation: 'bg-purple-100 text-purple-800',
testing: 'bg-orange-100 text-orange-800',
architecture: 'bg-red-100 text-red-800',
debugging: 'bg-yellow-100 text-yellow-800',
refactoring: 'bg-indigo-100 text-indigo-800',
general_chat: 'bg-gray-100 text-gray-800',
specialized_domain: 'bg-pink-100 text-pink-800'
};
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const [modelsRes, statusRes] = await Promise.all([
fetch('/api/ai-models/models'),
fetch('/api/ai-models/status')
]);
if (modelsRes.ok) {
const modelsData = await modelsRes.json();
setModels(modelsData);
}
if (statusRes.ok) {
const statusData = await statusRes.json();
setClusterStatus(statusData);
}
} catch (error) {
console.error('Error fetching AI models data:', error);
} finally {
setLoading(false);
}
};
const refreshModels = async () => {
setRefreshing(true);
try {
await fetch('/api/ai-models/refresh-models', { method: 'POST' });
await fetchData();
} catch (error) {
console.error('Error refreshing models:', error);
} finally {
setRefreshing(false);
}
};
const generateCompletion = async () => {
if (!prompt.trim()) return;
setGenerating(true);
try {
const response = await fetch('/api/ai-models/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
system_prompt: systemPrompt || undefined,
model_name: selectedModel || undefined,
task_type: taskType || undefined,
max_tokens: 1000,
temperature: 0.7
})
});
const data = await response.json();
setResponse(data);
} catch (error) {
console.error('Error generating completion:', error);
setResponse({
success: false,
error: 'Failed to generate completion',
model: selectedModel || 'unknown'
});
} finally {
setGenerating(false);
}
};
const generateCode = async () => {
if (!codeDescription.trim()) return;
setGenerating(true);
try {
const response = await fetch('/api/ai-models/code/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: codeDescription,
language: codeLanguage,
style: codeStyle,
max_tokens: 2000
})
});
const data = await response.json();
setCodeResponse(data);
} catch (error) {
console.error('Error generating code:', error);
setCodeResponse({
success: false,
error: 'Failed to generate code',
model: 'unknown'
});
} finally {
setGenerating(false);
}
};
const filteredModels = models.filter(model => {
const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
model.specialization?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCapability = selectedCapability === 'all' ||
model.capabilities.includes(selectedCapability);
return matchesSearch && matchesCapability;
});
const uniqueCapabilities = Array.from(
new Set(models.flatMap(m => m.capabilities))
).sort();
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<RefreshCw className="mx-auto h-12 w-12 animate-spin text-blue-500" />
<p className="mt-4 text-gray-600">Loading AI models...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">AI Models</h1>
<p className="mt-2 text-gray-600">
Manage and interact with the distributed Ollama cluster
</p>
</div>
<Button onClick={refreshModels} disabled={refreshing}>
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
Refresh Models
</Button>
</div>
{/* Cluster Status */}
{clusterStatus && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Activity className="h-8 w-8 text-green-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Healthy Nodes</p>
<p className="text-2xl font-bold text-gray-900">
{clusterStatus.healthy_nodes}/{clusterStatus.total_nodes}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Brain className="h-8 w-8 text-blue-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Models</p>
<p className="text-2xl font-bold text-gray-900">
{clusterStatus.total_models}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Gauge className="h-8 w-8 text-orange-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Cluster Load</p>
<p className="text-2xl font-bold text-gray-900">
{clusterStatus.cluster_load.toFixed(1)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Code className="h-8 w-8 text-purple-500" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Code Models</p>
<p className="text-2xl font-bold text-gray-900">
{clusterStatus.models_by_capability.code_generation || 0}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)}
<Tabs defaultValue="models" className="space-y-4">
<TabsList>
<TabsTrigger value="models">Available Models</TabsTrigger>
<TabsTrigger value="chat">AI Chat</TabsTrigger>
<TabsTrigger value="code">Code Generation</TabsTrigger>
</TabsList>
<TabsContent value="models" className="space-y-4">
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Search models..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Select value={selectedCapability} onValueChange={setSelectedCapability}>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="Filter by capability" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Capabilities</SelectItem>
{uniqueCapabilities.map(cap => (
<SelectItem key={cap} value={cap}>
{cap.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Models Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredModels.map((model) => (
<Card key={model.name} className={`${!model.availability ? 'opacity-50' : ''}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{model.name}</CardTitle>
<CardDescription>
{model.parameter_count} parameters {model.context_length.toLocaleString()} context
</CardDescription>
</div>
<Badge variant={model.availability ? "default" : "secondary"}>
{model.availability ? "Available" : "Offline"}
</Badge>
</div>
</CardHeader>
<CardContent>
{model.specialization && (
<p className="text-sm text-gray-600 mb-3">
Specialized in: {model.specialization}
</p>
)}
<div className="flex flex-wrap gap-1 mb-3">
{model.capabilities.map(cap => {
const IconComponent = capabilityIcons[cap] || Brain;
return (
<Badge
key={cap}
variant="outline"
className={`${capabilityColors[cap] || 'bg-gray-100 text-gray-800'} text-xs`}
>
<IconComponent className="w-3 h-3 mr-1" />
{cap.replace('_', ' ')}
</Badge>
);
})}
</div>
<div className="text-xs text-gray-500 space-y-1">
<div>Usage: {model.usage_count} requests</div>
{model.avg_response_time > 0 && (
<div>Avg Response: {model.avg_response_time.toFixed(2)}s</div>
)}
<div>Performance: {(model.performance_score * 100).toFixed(0)}%</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="chat" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>AI Chat Interface</CardTitle>
<CardDescription>
Interact with AI models for various tasks
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger>
<SelectValue placeholder="Select model (auto if empty)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Auto-select best model</SelectItem>
{models.filter(m => m.availability).map(model => (
<SelectItem key={model.name} value={model.name}>
{model.name} ({model.parameter_count})
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={taskType} onValueChange={setTaskType}>
<SelectTrigger>
<SelectValue placeholder="Task type (optional)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">Any task</SelectItem>
{uniqueCapabilities.map(cap => (
<SelectItem key={cap} value={cap}>
{cap.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Input
placeholder="System prompt (optional)"
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
/>
<Textarea
placeholder="Enter your prompt..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={4}
/>
<Button
onClick={generateCompletion}
disabled={generating || !prompt.trim()}
className="w-full"
>
{generating ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
<Zap className="mr-2 h-4 w-4" />
Generate Response
</>
)}
</Button>
{response && (
<Alert className={response.success ? "border-green-200" : "border-red-200"}>
<AlertDescription>
{response.success ? (
<div>
<div className="font-semibold mb-2">
Response from {response.model}
{response.response_time && ` (${response.response_time.toFixed(2)}s)`}:
</div>
<pre className="whitespace-pre-wrap text-sm bg-gray-50 p-3 rounded">
{response.content}
</pre>
</div>
) : (
<div className="text-red-600">
Error: {response.error}
</div>
)}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="code" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Code Generation</CardTitle>
<CardDescription>
Generate code using specialized programming models
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select value={codeLanguage} onValueChange={setCodeLanguage}>
<SelectTrigger>
<SelectValue placeholder="Programming language" />
</SelectTrigger>
<SelectContent>
<SelectItem value="python">Python</SelectItem>
<SelectItem value="javascript">JavaScript</SelectItem>
<SelectItem value="typescript">TypeScript</SelectItem>
<SelectItem value="rust">Rust</SelectItem>
<SelectItem value="go">Go</SelectItem>
<SelectItem value="java">Java</SelectItem>
<SelectItem value="cpp">C++</SelectItem>
<SelectItem value="bash">Bash</SelectItem>
</SelectContent>
</Select>
<Select value={codeStyle} onValueChange={setCodeStyle}>
<SelectTrigger>
<SelectValue placeholder="Code style" />
</SelectTrigger>
<SelectContent>
<SelectItem value="clean">Clean & Readable</SelectItem>
<SelectItem value="optimized">Performance Optimized</SelectItem>
<SelectItem value="documented">Well Documented</SelectItem>
</SelectContent>
</Select>
</div>
<Textarea
placeholder="Describe the code you want to generate..."
value={codeDescription}
onChange={(e) => setCodeDescription(e.target.value)}
rows={4}
/>
<Button
onClick={generateCode}
disabled={generating || !codeDescription.trim()}
className="w-full"
>
{generating ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Generating Code...
</>
) : (
<>
<Code className="mr-2 h-4 w-4" />
Generate Code
</>
)}
</Button>
{codeResponse && (
<Alert className={codeResponse.success ? "border-green-200" : "border-red-200"}>
<AlertDescription>
{codeResponse.success ? (
<div>
<div className="font-semibold mb-2">
Code generated using {codeResponse.model}
{codeResponse.response_time && ` (${codeResponse.response_time.toFixed(2)}s)`}:
</div>
<pre className="whitespace-pre-wrap text-sm bg-gray-900 text-green-400 p-4 rounded overflow-x-auto">
{codeResponse.content}
</pre>
</div>
) : (
<div className="text-red-600">
Error: {codeResponse.error}
</div>
)}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default AIModels;

View File

@@ -0,0 +1,514 @@
import React, { useState, useEffect } from 'react';
import {
UserGroupIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon,
CpuChipIcon,
CommandLineIcon,
DocumentTextIcon,
ArrowPathIcon,
ExclamationTriangleIcon,
ChatBubbleLeftRightIcon
} from '@heroicons/react/24/outline';
interface TeamMember {
agent_id: string;
role: string;
endpoint: string;
capabilities: string[];
status: string;
}
interface Decision {
decision_id?: string;
id?: string;
title: string;
description?: string;
author_role: string;
timestamp: string;
ucxl_address?: string;
}
interface TeamStatus {
total_members: number;
online_members: number;
offline_members: number;
role_distribution: Record<string, number>;
active_decisions: number;
recent_decisions: Decision[];
network_health: number;
}
interface TaskAssignment {
task_description: string;
required_capabilities: string[];
priority: 'low' | 'medium' | 'high' | 'urgent';
}
const BzzzTeam: React.FC = () => {
const [teamStatus, setTeamStatus] = useState<TeamStatus | null>(null);
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [recentDecisions, setRecentDecisions] = useState<Decision[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Task assignment form
const [showTaskForm, setShowTaskForm] = useState(false);
const [taskForm, setTaskForm] = useState<TaskAssignment>({
task_description: '',
required_capabilities: [],
priority: 'medium'
});
// Decision publishing form
const [showDecisionForm, setShowDecisionForm] = useState(false);
const [decisionForm, setDecisionForm] = useState({
title: '',
description: '',
context: {},
ucxl_address: ''
});
useEffect(() => {
loadBzzzData();
const interval = setInterval(loadBzzzData, 30000); // Update every 30 seconds
return () => clearInterval(interval);
}, []);
const loadBzzzData = async () => {
try {
setError(null);
// Load team status
const statusResponse = await fetch('/api/bzzz/status');
if (statusResponse.ok) {
const status = await statusResponse.json();
setTeamStatus(status);
}
// Load team members
const membersResponse = await fetch('/api/bzzz/members');
if (membersResponse.ok) {
const members = await membersResponse.json();
setTeamMembers(members);
}
// Load recent decisions
const decisionsResponse = await fetch('/api/bzzz/decisions?limit=10');
if (decisionsResponse.ok) {
const decisions = await decisionsResponse.json();
setRecentDecisions(decisions);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load BZZZ data');
} finally {
setIsLoading(false);
}
};
const handleTaskAssignment = async () => {
try {
const response = await fetch('/api/bzzz/tasks/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskForm)
});
if (response.ok) {
const result = await response.json();
alert(`Task assigned to: ${result.assigned_to}`);
setShowTaskForm(false);
setTaskForm({ task_description: '', required_capabilities: [], priority: 'medium' });
loadBzzzData(); // Refresh data
} else {
throw new Error('Failed to assign task');
}
} catch (err) {
alert(`Error: ${err instanceof Error ? err.message : 'Failed to assign task'}`);
}
};
const handleDecisionPublish = async () => {
try {
const response = await fetch('/api/bzzz/decisions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(decisionForm)
});
if (response.ok) {
const result = await response.json();
alert(`Decision published with ID: ${result.decision_id}`);
setShowDecisionForm(false);
setDecisionForm({ title: '', description: '', context: {}, ucxl_address: '' });
loadBzzzData(); // Refresh data
} else {
throw new Error('Failed to publish decision');
}
} catch (err) {
alert(`Error: ${err instanceof Error ? err.message : 'Failed to publish decision'}`);
}
};
const rediscoverNetwork = async () => {
try {
const response = await fetch('/api/bzzz/network/discover', { method: 'POST' });
if (response.ok) {
await loadBzzzData();
alert('Network rediscovery completed');
} else {
throw new Error('Failed to rediscover network');
}
} catch (err) {
alert(`Error: ${err instanceof Error ? err.message : 'Failed to rediscover network'}`);
}
};
const getRoleIcon = (role: string) => {
switch (role) {
case 'senior_architect': return '🏗️';
case 'frontend_developer': return '🎨';
case 'backend_developer': return '⚙️';
case 'devops_engineer': return '🚀';
case 'project_manager': return '👑';
case 'ai_coordinator': return '🧠';
default: return '👤';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'text-green-500';
case 'offline': return 'text-red-500';
case 'busy': return 'text-yellow-500';
default: return 'text-gray-500';
}
};
const getHealthColor = (health: number) => {
if (health >= 0.8) return 'text-green-500';
if (health >= 0.5) return 'text-yellow-500';
return 'text-red-500';
};
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading BZZZ Team...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
<UserGroupIcon className="w-8 h-8 mr-3 text-blue-600" />
BZZZ Team Collaboration
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">
Distributed AI team coordination and decision consensus
</p>
</div>
<div className="flex space-x-3">
<button
onClick={rediscoverNetwork}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center"
>
<ArrowPathIcon className="w-4 h-4 mr-2" />
Rediscover
</button>
</div>
</div>
</div>
{error && (
<div className="mb-6 bg-red-100 dark:bg-red-900 border border-red-400 text-red-700 dark:text-red-200 px-4 py-3 rounded">
<div className="flex items-center">
<ExclamationTriangleIcon className="w-5 h-5 mr-2" />
{error}
</div>
</div>
)}
{/* Network Status Cards */}
{teamStatus && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<UserGroupIcon className="w-8 h-8 text-blue-600" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Team Members</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{teamStatus.online_members}/{teamStatus.total_members}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<CheckCircleIcon className={`w-8 h-8 ${getHealthColor(teamStatus.network_health)}`} />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Network Health</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{(teamStatus.network_health * 100).toFixed(0)}%
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<DocumentTextIcon className="w-8 h-8 text-green-600" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Active Decisions</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{teamStatus.active_decisions}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<div className="flex items-center">
<CpuChipIcon className="w-8 h-8 text-purple-600" />
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Role Types</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{Object.keys(teamStatus.role_distribution).length}
</p>
</div>
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className="mb-8">
<div className="flex space-x-4">
<button
onClick={() => setShowTaskForm(true)}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center"
>
<CommandLineIcon className="w-5 h-5 mr-2" />
Assign Task
</button>
<button
onClick={() => setShowDecisionForm(true)}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center"
>
<ChatBubbleLeftRightIcon className="w-5 h-5 mr-2" />
Publish Decision
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Team Members */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Team Members</h3>
</div>
<div className="p-6">
<div className="space-y-4">
{teamMembers.map((member) => (
<div key={member.agent_id} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center">
<span className="text-2xl mr-3">{getRoleIcon(member.role)}</span>
<div>
<p className="font-medium text-gray-900 dark:text-white">{member.agent_id}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">{member.role.replace('_', ' ')}</p>
<div className="flex flex-wrap gap-1 mt-1">
{member.capabilities.slice(0, 3).map((cap) => (
<span key={cap} className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
{cap}
</span>
))}
{member.capabilities.length > 3 && (
<span className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded">
+{member.capabilities.length - 3} more
</span>
)}
</div>
</div>
</div>
<div className={`flex items-center ${getStatusColor(member.status)}`}>
{member.status === 'online' ? <CheckCircleIcon className="w-5 h-5" /> : <XCircleIcon className="w-5 h-5" />}
<span className="ml-1 text-sm font-medium">{member.status}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Recent Decisions */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Recent Decisions</h3>
</div>
<div className="p-6">
<div className="space-y-4">
{recentDecisions.map((decision) => (
<div key={decision.decision_id || decision.id} className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white">{decision.title}</h4>
{decision.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{decision.description}</p>
)}
<div className="flex items-center mt-2 text-xs text-gray-500 dark:text-gray-400">
<span className="mr-1">{getRoleIcon(decision.author_role)}</span>
<span className="mr-3">{decision.author_role.replace('_', ' ')}</span>
<ClockIcon className="w-4 h-4 mr-1" />
<span>{new Date(decision.timestamp).toLocaleString()}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Task Assignment Modal */}
{showTaskForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Assign Task</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Task Description
</label>
<textarea
value={taskForm.task_description}
onChange={(e) => setTaskForm({...taskForm, task_description: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Required Capabilities (comma-separated)
</label>
<input
type="text"
value={taskForm.required_capabilities.join(', ')}
onChange={(e) => setTaskForm({
...taskForm,
required_capabilities: e.target.value.split(',').map(s => s.trim()).filter(s => s)
})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
placeholder="frontend, backend, devops"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Priority
</label>
<select
value={taskForm.priority}
onChange={(e) => setTaskForm({...taskForm, priority: e.target.value as any})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={() => setShowTaskForm(false)}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white"
>
Cancel
</button>
<button
onClick={handleTaskAssignment}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
Assign Task
</button>
</div>
</div>
</div>
)}
{/* Decision Publishing Modal */}
{showDecisionForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Publish Decision</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Title
</label>
<input
type="text"
value={decisionForm.title}
onChange={(e) => setDecisionForm({...decisionForm, title: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea
value={decisionForm.description}
onChange={(e) => setDecisionForm({...decisionForm, description: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
UCXL Address (optional)
</label>
<input
type="text"
value={decisionForm.ucxl_address}
onChange={(e) => setDecisionForm({...decisionForm, ucxl_address: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
placeholder="ucxl://any:any@PROJECT:COMPONENT/path"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={() => setShowDecisionForm(false)}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white"
>
Cancel
</button>
<button
onClick={handleDecisionPublish}
className="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"
>
Publish Decision
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default BzzzTeam;

View File

@@ -92,7 +92,7 @@ export default function Dashboard() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">
Welcome to Hive
Welcome to WHOOSH
</h1>
<p className="text-gray-600 mt-2">
Monitor your distributed AI orchestration platform

View File

@@ -0,0 +1,592 @@
/**
* Git Repositories Management Page
* Allows users to add, manage, and configure git repositories for their projects
*/
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import {
GitBranch,
Plus,
Settings,
RefreshCw,
Trash2,
FolderOpen,
FileText,
ExternalLink,
Key,
Lock,
Unlock,
CheckCircle,
AlertCircle,
Loader2,
Eye,
EyeOff
} from 'lucide-react';
import { apiConfig } from '../config/api';
import { useAuthenticatedFetch } from '../contexts/AuthContext';
interface GitRepository {
id: string;
name: string;
url: string;
project_id?: string;
local_path?: string;
default_branch: string;
status: string;
last_updated?: string;
commit_hash?: string;
commit_message?: string;
error_message?: string;
credentials: {
auth_type: string;
has_username: boolean;
has_password: boolean;
has_ssh_key: boolean;
};
}
interface Project {
id: string;
name: string;
}
export const GitRepositories: React.FC = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [showAddForm, setShowAddForm] = useState(false);
const [formData, setFormData] = useState({
name: '',
url: '',
project_id: '',
auth_type: 'https',
username: '',
password: '',
ssh_key_content: ''
});
const [formLoading, setFormLoading] = useState(false);
const [error, setError] = useState<string>('');
const [showPassword, setShowPassword] = useState(false);
const [selectedRepo, setSelectedRepo] = useState<GitRepository | null>(null);
const [showFiles, setShowFiles] = useState<string | null>(null);
const [fileStructure, setFileStructure] = useState<any>(null);
const authenticatedFetch = useAuthenticatedFetch();
const API_BASE_URL = apiConfig.baseURL + '/api';
useEffect(() => {
loadRepositories();
loadProjects();
}, []);
const loadRepositories = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/`);
const data = await response.json();
if (data.success) {
setRepositories(data.data.repositories);
} else {
setError('Failed to load repositories');
}
} catch (err: any) {
setError(`Error loading repositories: ${err.message}`);
} finally {
setLoading(false);
}
};
const loadProjects = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/projects`);
const data = await response.json();
if (data.success) {
setProjects(data.data.projects || []);
}
} catch (err: any) {
console.error('Error loading projects:', err);
}
};
const handleAddRepository = async () => {
try {
setFormLoading(true);
setError('');
const credentials = {
auth_type: formData.auth_type,
username: formData.username || undefined,
password: formData.password || undefined,
ssh_key_content: formData.ssh_key_content || undefined
};
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.name,
url: formData.url,
project_id: formData.project_id || undefined,
credentials
})
});
const data = await response.json();
if (data.success) {
setShowAddForm(false);
setFormData({
name: '',
url: '',
project_id: '',
auth_type: 'https',
username: '',
password: '',
ssh_key_content: ''
});
loadRepositories();
} else {
setError(data.data?.error || 'Failed to add repository');
}
} catch (err: any) {
setError(`Error adding repository: ${err.message}`);
} finally {
setFormLoading(false);
}
};
const handleUpdateRepository = async (repoId: string) => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}/update`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
loadRepositories();
} else {
setError(data.data?.error || 'Failed to update repository');
}
} catch (err: any) {
setError(`Error updating repository: ${err.message}`);
}
};
const handleRemoveRepository = async (repoId: string) => {
if (!confirm('Are you sure you want to remove this repository?')) return;
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
loadRepositories();
} else {
setError(data.data?.error || 'Failed to remove repository');
}
} catch (err: any) {
setError(`Error removing repository: ${err.message}`);
}
};
const loadFileStructure = async (repoId: string) => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/git-repositories/${repoId}/files`);
const data = await response.json();
if (data.success) {
setFileStructure(data.data.structure);
setShowFiles(repoId);
} else {
setError(data.data?.error || 'Failed to load file structure');
}
} catch (err: any) {
setError(`Error loading files: ${err.message}`);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'ready':
return <CheckCircle className="w-4 h-4 text-green-600" />;
case 'cloning':
return <Loader2 className="w-4 h-4 text-blue-600 animate-spin" />;
case 'error':
return <AlertCircle className="w-4 h-4 text-red-600" />;
default:
return <AlertCircle className="w-4 h-4 text-gray-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'ready': return 'bg-green-100 text-green-800';
case 'cloning': return 'bg-blue-100 text-blue-800';
case 'error': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getAuthIcon = (authType: string) => {
switch (authType) {
case 'ssh': return <Key className="w-4 h-4" />;
case 'https': return <Lock className="w-4 h-4" />;
default: return <Unlock className="w-4 h-4" />;
}
};
if (loading) {
return (
<div className="p-6">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
<span className="ml-2 text-gray-600">Loading repositories...</span>
</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Git Repositories</h1>
<p className="text-gray-600 mt-2">
Manage git repositories for your projects with secure credential storage
</p>
</div>
<Dialog open={showAddForm} onOpenChange={setShowAddForm}>
<DialogTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
Add Repository
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Git Repository</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="name">Repository Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="My Project Repository"
/>
</div>
<div>
<Label htmlFor="project">Project (Optional)</Label>
<Select
value={formData.project_id}
onValueChange={(value) => setFormData(prev => ({ ...prev, project_id: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">No project</SelectItem>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="url">Repository URL</Label>
<Input
id="url"
value={formData.url}
onChange={(e) => setFormData(prev => ({ ...prev, url: e.target.value }))}
placeholder="https://github.com/user/repo.git or git@github.com:user/repo.git"
/>
</div>
<div>
<Label htmlFor="auth_type">Authentication Type</Label>
<Select
value={formData.auth_type}
onValueChange={(value) => setFormData(prev => ({ ...prev, auth_type: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="https">HTTPS (Username/Password)</SelectItem>
<SelectItem value="ssh">SSH Key</SelectItem>
<SelectItem value="token">Personal Access Token</SelectItem>
</SelectContent>
</Select>
</div>
{formData.auth_type === 'https' && (
<>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="Git username"
/>
</div>
<div>
<Label htmlFor="password">Password/Token</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="Password or personal access token"
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
</div>
</>
)}
{formData.auth_type === 'ssh' && (
<div>
<Label htmlFor="ssh_key">SSH Private Key</Label>
<Textarea
id="ssh_key"
value={formData.ssh_key_content}
onChange={(e) => setFormData(prev => ({ ...prev, ssh_key_content: e.target.value }))}
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
rows={8}
/>
<p className="text-sm text-gray-500 mt-1">
Paste your private SSH key content here
</p>
</div>
)}
{formData.auth_type === 'token' && (
<div>
<Label htmlFor="token">Personal Access Token</Label>
<div className="relative">
<Input
id="token"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-sm text-gray-500 mt-1">
Generate a personal access token from your git provider
</p>
</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowAddForm(false)}>
Cancel
</Button>
<Button
onClick={handleAddRepository}
disabled={formLoading || !formData.name || !formData.url}
>
{formLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding...
</>
) : (
'Add Repository'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{repositories.length === 0 ? (
<Card className="col-span-full">
<CardContent className="text-center py-12">
<GitBranch className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Repositories</h3>
<p className="text-gray-600 mb-4">
Add your first git repository to start working with your codebase
</p>
<Button onClick={() => setShowAddForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Repository
</Button>
</CardContent>
</Card>
) : (
repositories.map((repo) => (
<Card key={repo.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="flex items-center gap-2">
<GitBranch className="w-5 h-5" />
{repo.name}
<Badge className={`ml-2 ${getStatusColor(repo.status)}`}>
{getStatusIcon(repo.status)}
<span className="ml-1">{repo.status}</span>
</Badge>
</CardTitle>
<p className="text-sm text-gray-600 mt-1">{repo.url}</p>
</div>
<div className="flex items-center gap-2">
{getAuthIcon(repo.credentials.auth_type)}
<span className="text-xs text-gray-500">{repo.credentials.auth_type}</span>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{repo.commit_hash && (
<div className="text-sm">
<span className="font-medium">Latest commit:</span>
<div className="mt-1 p-2 bg-gray-50 rounded text-xs">
<div className="font-mono text-gray-700">{repo.commit_hash.substring(0, 8)}</div>
{repo.commit_message && (
<div className="text-gray-600 mt-1">{repo.commit_message}</div>
)}
</div>
</div>
)}
{repo.error_message && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">{repo.error_message}</AlertDescription>
</Alert>
)}
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleUpdateRepository(repo.id)}
disabled={repo.status === 'cloning'}
>
<RefreshCw className="w-4 h-4 mr-1" />
Update
</Button>
<Button
variant="outline"
size="sm"
onClick={() => loadFileStructure(repo.id)}
disabled={repo.status !== 'ready'}
>
<FolderOpen className="w-4 h-4 mr-1" />
Browse
</Button>
<Button
variant="outline"
size="sm"
onClick={() => window.open(repo.url, '_blank')}
>
<ExternalLink className="w-4 h-4 mr-1" />
Remote
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleRemoveRepository(repo.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4 mr-1" />
Remove
</Button>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* File Browser Modal */}
{showFiles && fileStructure && (
<Dialog open={!!showFiles} onOpenChange={() => setShowFiles(null)}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Repository Files</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<div className="text-sm text-gray-600 mb-4">
Browsing: {repositories.find(r => r.id === showFiles)?.name}
</div>
<div className="border rounded p-4 bg-gray-50 font-mono text-sm">
<pre>{JSON.stringify(fileStructure, null, 2)}</pre>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
};
export default GitRepositories;

View File

@@ -9,7 +9,7 @@ import {
KeyIcon,
ExclamationCircleIcon
} from '@heroicons/react/24/outline';
import HiveLogo from '../assets/Hive_symbol.png';
import WHOOSHLogo from '../assets/WHOOSH_symbol.png';
interface LoginCredentials {
username: string;
@@ -63,13 +63,13 @@ export default function Login() {
<div>
<div className="mx-auto h-16 w-16 flex items-center justify-center">
<img
src={HiveLogo}
alt="Hive Logo"
src={WHOOSHLogo}
alt="WHOOSH Logo"
className="h-16 w-16 object-contain"
/>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Sign in to Hive
Sign in to WHOOSH
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Distributed AI Management Platform
@@ -199,7 +199,7 @@ export default function Login() {
<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 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>
<p>Password: <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">whooshadmin123</code></p>
</div>
</div>
</form>

View File

@@ -111,7 +111,7 @@ export default function Settings() {
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
<p className="text-gray-600 mt-2">
Configure and manage your Hive distributed AI platform
Configure and manage your WHOOSH distributed AI platform
</p>
</div>
@@ -163,7 +163,7 @@ export default function Settings() {
// General Settings Component
function GeneralSettings() {
const [settings, setSettings] = useState({
systemName: 'Hive Development Cluster',
systemName: 'WHOOSH Development Cluster',
description: 'Distributed AI development platform for collaborative coding',
timezone: 'Australia/Melbourne',
language: 'en-US',
@@ -427,7 +427,7 @@ function UserManagementSettings() {
</div>
<div className="ml-3">
<div className="text-sm font-medium text-gray-900">Administrator</div>
<div className="text-sm text-gray-500">admin@hive.local</div>
<div className="text-sm text-gray-500">admin@whoosh.local</div>
</div>
</div>
</td>
@@ -580,7 +580,7 @@ function NotificationSettings() {
</label>
<input
type="url"
placeholder="https://your-webhook-endpoint.com/hive"
placeholder="https://your-webhook-endpoint.com/whoosh"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>

View File

@@ -62,7 +62,7 @@ export default function SystemLogs() {
const generateMockLogs = (): LogEntry[] => {
const levels: LogEntry['level'][] = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'];
const components = [
'hive-coordinator', 'agent-manager', 'workflow-engine', 'api-gateway',
'whoosh-coordinator', 'agent-manager', 'workflow-engine', 'api-gateway',
'auth-service', 'task-executor', 'metrics-collector', 'websocket-server'
];
@@ -154,7 +154,7 @@ ${level}Exception: ${message}
const generateMockStats = (): LogStats => {
const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'];
const components = [
'hive-coordinator', 'agent-manager', 'workflow-engine', 'api-gateway',
'whoosh-coordinator', 'agent-manager', 'workflow-engine', 'api-gateway',
'auth-service', 'task-executor', 'metrics-collector', 'websocket-server'
];

View File

@@ -5,13 +5,13 @@ import { Button } from '../components/ui/button';
<Meta title="Introduction/Welcome" />
# Hive UI Component Library
# WHOOSH UI Component Library
Welcome to the **Hive UI Component Library** documentation! This Storybook contains all the reusable components used throughout the Hive Distributed AI Orchestration Platform.
Welcome to the **WHOOSH UI Component Library** documentation! This Storybook contains all the reusable components used throughout the WHOOSH Distributed AI Orchestration Platform.
## About Hive
## About WHOOSH
Hive is a sophisticated distributed AI orchestration platform that enables seamless coordination of multiple AI agents across different machines and services. The platform provides:
WHOOSH is a sophisticated distributed AI orchestration platform that enables seamless coordination of multiple AI agents across different machines and services. The platform provides:
- **Agent Management**: Register and manage AI agents across your cluster
- **Task Orchestration**: Intelligent task distribution and execution

View File

@@ -13,7 +13,7 @@ export interface Project {
id: string;
name: string;
description?: string;
status: 'active' | 'inactive' | 'archived';
status: 'active' | 'inactive' | 'arcwhooshd';
created_at: string;
updated_at: string;
metadata?: Record<string, any>;
@@ -58,7 +58,7 @@ export interface CreateProjectRequest {
export interface UpdateProjectRequest {
name?: string;
description?: string;
status?: 'active' | 'inactive' | 'archived';
status?: 'active' | 'inactive' | 'arcwhooshd';
tags?: string[];
metadata?: Record<string, any>;
bzzz_config?: BzzzConfig;