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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
53
frontend/Dockerfile.prod
Normal 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;"]
|
||||
@@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
1
frontend/dist/assets/index-CZWs29Ng.css
vendored
Normal file
1
frontend/dist/assets/index-CZWs29Ng.css
vendored
Normal file
File diff suppressed because one or more lines are too long
529
frontend/dist/assets/index-DVsl2bkP.js
vendored
Normal file
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
82
frontend/dist/index.html
vendored
Normal 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>
|
||||
@@ -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
116
frontend/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
2
frontend/node_modules/.package-lock.json
generated
vendored
2
frontend/node_modules/.package-lock.json
generated
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "hive-frontend",
|
||||
"name": "whoosh-frontend",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
|
||||
58
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
58
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -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
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
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
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
BIN
frontend/src/assets/WHOOSH_symbol.png
Normal file
BIN
frontend/src/assets/WHOOSH_symbol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
264
frontend/src/components/members/MemberDashboard.tsx
Normal file
264
frontend/src/components/members/MemberDashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
322
frontend/src/components/members/MemberInviteForm.tsx
Normal file
322
frontend/src/components/members/MemberInviteForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
373
frontend/src/components/members/MemberList.tsx
Normal file
373
frontend/src/components/members/MemberList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
1092
frontend/src/components/projects/ProjectSetupWizard.tsx
Normal file
1092
frontend/src/components/projects/ProjectSetupWizard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
116
frontend/src/components/setup/ClusterDetector.tsx
Normal file
116
frontend/src/components/setup/ClusterDetector.tsx
Normal 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;
|
||||
1051
frontend/src/components/setup/SetupWizard.tsx
Normal file
1051
frontend/src/components/setup/SetupWizard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
421
frontend/src/components/templates/TemplateBrowser.tsx
Normal file
421
frontend/src/components/templates/TemplateBrowser.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
277
frontend/src/components/templates/TemplateSelector.tsx
Normal file
277
frontend/src/components/templates/TemplateSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
581
frontend/src/pages/AIModels.tsx
Normal file
581
frontend/src/pages/AIModels.tsx
Normal 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;
|
||||
514
frontend/src/pages/BzzzTeam.tsx
Normal file
514
frontend/src/pages/BzzzTeam.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
592
frontend/src/pages/GitRepositories.tsx
Normal file
592
frontend/src/pages/GitRepositories.tsx
Normal 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----- ... -----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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user