fix: NODE1_REPAIR - healthchecks, dependencies, SSR env, telegram gateway
TASK_PHASE_NODE1_REPAIR: - Fix daarion-web SSR: use CITY_API_BASE_URL instead of 127.0.0.1 - Fix auth API routes: use AUTH_API_URL env var - Add wget to Dockerfiles for healthchecks (stt, ocr, web-search, swapper, vector-db, rag) - Update healthchecks to use wget instead of curl - Fix vector-db-service: update torch==2.4.0, sentence-transformers==2.6.1 - Fix rag-service: correct haystack imports for v2.x - Fix telegram-gateway: remove msg.ack() for non-JetStream NATS - Add /health endpoint to nginx mvp-routes.conf - Add room_role, is_public, sort_order columns to city_rooms migration - Add TASK_PHASE_NODE1_REPAIR.md and DEPLOY_NODE1_REPAIR.md docs Previous tasks included: - TASK 039-044: Orchestrator rooms, Matrix chat cleanup, CrewAI integration
This commit is contained in:
@@ -58,8 +58,13 @@ function App() {
|
||||
<Route path="/secondme" element={<SecondMePage />} />
|
||||
<Route path="/space" element={<SpaceDashboard />} />
|
||||
<Route path="/messenger" element={<MessengerPage />} />
|
||||
{/* Task 039: Agent Console v2 */}
|
||||
<Route path="/agents" element={<AgentHubPage />} />
|
||||
<Route path="/agents/:agentId" element={<AgentCabinet />} />
|
||||
{/* Legacy Aliases */}
|
||||
<Route path="/agent-hub" element={<AgentHubPage />} />
|
||||
<Route path="/agent/:agentId" element={<AgentCabinet />} />
|
||||
|
||||
<Route path="/microdao" element={<MicrodaoListPage />} />
|
||||
<Route path="/microdao/:slug" element={<MicrodaoConsolePage />} />
|
||||
<Route path="/dao" element={<DaoListPage />} />
|
||||
@@ -81,7 +86,6 @@ function App() {
|
||||
<Route path="/microdao/energy-union" element={<EnergyUnionCabinetPage />} />
|
||||
<Route path="/microdao/yaromir" element={<YaromirCabinetPage />} />
|
||||
<Route path="/dagi-monitor" element={<DagiMonitorPage />} />
|
||||
<Route path="/agent/:agentId" element={<AgentCabinetPage />} />
|
||||
<Route path="/chat-demo" element={<ChatDemoPage />} />
|
||||
<Route path="/network" element={<NetworkPageSimple />} />
|
||||
<Route path="/connect-node" element={<ConnectNodePage />} />
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* API: http://localhost:7014
|
||||
*/
|
||||
|
||||
import { apiGet, apiPut, apiPost, apiPatch, apiDelete } from './client';
|
||||
import type { AgentSummary, AgentDetailDashboard, AgentVisibilityPayload } from '../types/agent-cabinet';
|
||||
|
||||
// ============================================================================
|
||||
// Types (matching backend models.py)
|
||||
// ============================================================================
|
||||
@@ -197,7 +200,38 @@ async function agentsRequest<T>(
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Agent CRUD
|
||||
// Task 039: Unified Agent API (City Service)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get list of agents with filtering (Unified API)
|
||||
*/
|
||||
export async function getAgentList(params: Record<string, any> = {}): Promise<{ items: AgentSummary[], total: number }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
return apiGet<{ items: AgentSummary[], total: number }>(`/city/public/agents?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Agent Cabinet Dashboard (Unified API)
|
||||
*/
|
||||
export async function getAgentDashboard(agentId: string): Promise<AgentDetailDashboard> {
|
||||
return apiGet<AgentDetailDashboard>(`/city/city/agents/${encodeURIComponent(agentId)}/dashboard`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Agent Visibility (Unified API)
|
||||
*/
|
||||
export async function updateAgentVisibility(agentId: string, payload: AgentVisibilityPayload): Promise<any> {
|
||||
return apiPut(`/city/city/agents/${encodeURIComponent(agentId)}/visibility`, payload);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Legacy Agent CRUD (Agents Service)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,12 @@ export interface CityRoom {
|
||||
last_event?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
logo_url?: string | null;
|
||||
banner_url?: string | null;
|
||||
microdao_id?: string;
|
||||
microdao_name?: string;
|
||||
microdao_slug?: string;
|
||||
microdao_logo_url?: string;
|
||||
}
|
||||
|
||||
export interface CityRoomMessage {
|
||||
|
||||
@@ -93,6 +93,14 @@ export async function apiPost<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPut<T>(endpoint: string, data: unknown): Promise<T> {
|
||||
const response = await fetchApi(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPatch<T>(endpoint: string, data: unknown): Promise<T> {
|
||||
const response = await fetchApi(endpoint, {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface MicrodaoRead extends MicrodaoBase {
|
||||
updated_at: string;
|
||||
member_count?: number;
|
||||
agent_count?: number;
|
||||
logo_url?: string | null;
|
||||
banner_url?: string | null;
|
||||
}
|
||||
|
||||
export interface MicrodaoCreate extends MicrodaoBase {}
|
||||
@@ -240,3 +242,48 @@ export async function checkMicrodaoServiceHealth(): Promise<{
|
||||
return microdaoRequest('/health');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Assets & Branding
|
||||
// ============================================================================
|
||||
|
||||
export interface AssetUploadResponse {
|
||||
original_url: string;
|
||||
processed_url: string;
|
||||
thumb_url: string;
|
||||
}
|
||||
|
||||
export async function uploadAsset(file: File, type: string): Promise<AssetUploadResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('type', type);
|
||||
|
||||
const sessionToken = localStorage.getItem('daarion_session_token');
|
||||
const headers: HeadersInit = {};
|
||||
if (sessionToken) {
|
||||
headers['Authorization'] = `Bearer ${sessionToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${MICRODAO_API_URL}/assets/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload asset');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateMicrodaoBranding(
|
||||
slug: string,
|
||||
logoUrl?: string | null,
|
||||
bannerUrl?: string | null
|
||||
): Promise<MicrodaoRead> {
|
||||
return microdaoRequest<MicrodaoRead>(`/microdao/${encodeURIComponent(slug)}/branding`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ logo_url: logoUrl, banner_url: bannerUrl })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
46
src/api/microdaoWizard.ts
Normal file
46
src/api/microdaoWizard.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { apiPost } from './client';
|
||||
|
||||
export interface CreateMicroDaoPayload {
|
||||
agent_id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
visibility: 'public' | 'confidential';
|
||||
district?: string;
|
||||
create_rooms: {
|
||||
primary_lobby: boolean;
|
||||
governance: boolean;
|
||||
crew_team: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AttachAgentPayload {
|
||||
agent_id: string;
|
||||
role: 'orchestrator' | 'member';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MicroDAO from Agent (Wizard)
|
||||
*/
|
||||
export async function createMicroDaoFromAgent(payload: CreateMicroDaoPayload): Promise<any> {
|
||||
const { agent_id, ...data } = payload;
|
||||
// Map frontend payload to backend expectations
|
||||
const backendPayload = {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description,
|
||||
is_public: data.visibility === 'public',
|
||||
make_platform: !!data.district, // Simplified logic for MVP
|
||||
create_rooms: data.create_rooms
|
||||
};
|
||||
|
||||
return apiPost(`/city/city/agents/${encodeURIComponent(agent_id)}/microdao`, backendPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Agent to existing MicroDAO
|
||||
*/
|
||||
export async function attachAgentToMicroDao(slug: string, payload: AttachAgentPayload): Promise<any> {
|
||||
return apiPost(`/city/city/microdao/${encodeURIComponent(slug)}/attach-agent`, payload);
|
||||
}
|
||||
|
||||
@@ -1,300 +1,257 @@
|
||||
/**
|
||||
* AgentCabinet Component
|
||||
* Full agent view with tabs: Metrics, Context, Settings
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useAgent } from './hooks/useAgent';
|
||||
import { useAgentContext } from './hooks/useAgentContext';
|
||||
import { AgentMetricsPanel } from './AgentMetricsPanel';
|
||||
import { AgentSettingsPanel } from './AgentSettingsPanel';
|
||||
import { AgentEventsPanel } from './AgentEventsPanel';
|
||||
|
||||
type TabType = 'metrics' | 'context' | 'settings' | 'events';
|
||||
|
||||
const STATUS_COLORS = {
|
||||
active: 'bg-green-500',
|
||||
idle: 'bg-yellow-500',
|
||||
offline: 'bg-gray-400',
|
||||
error: 'bg-red-500',
|
||||
};
|
||||
|
||||
const STATUS_LABELS = {
|
||||
active: 'Активний',
|
||||
idle: 'Очікує',
|
||||
offline: 'Офлайн',
|
||||
error: 'Помилка',
|
||||
};
|
||||
import { useAgentDashboard } from './hooks/useAgentDashboard';
|
||||
import { VisibilityCard } from './components/VisibilityCard';
|
||||
import { AgentChatWidget } from './components/AgentChatWidget';
|
||||
import { MicroDaoWizard } from './components/MicroDaoWizard';
|
||||
|
||||
export function AgentCabinet() {
|
||||
const { agentId } = useParams<{ agentId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('metrics');
|
||||
|
||||
const { agent, loading, error, refetch } = useAgent(agentId!);
|
||||
const { context, loading: contextLoading } = useAgentContext(agentId!);
|
||||
const { dashboard, loading, error, refetch } = useAgentDashboard(agentId!);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 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 mb-4" />
|
||||
<div className="text-gray-600">Завантаження агента...</div>
|
||||
<div className="text-gray-600">Loading Agent Cabinet...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !agent) {
|
||||
if (error || !dashboard) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8 text-center max-w-md">
|
||||
<div className="text-6xl mb-4">❌</div>
|
||||
<h2 className="text-xl font-semibold text-red-900 mb-2">
|
||||
Агент не знайдено
|
||||
</h2>
|
||||
<p className="text-red-600 mb-4">
|
||||
{error?.message || 'Агент не існує або недоступний'}
|
||||
</p>
|
||||
<h2 className="text-xl font-semibold text-red-900 mb-2">Agent Not Found</h2>
|
||||
<button
|
||||
onClick={() => navigate('/agent-hub')}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
← Повернутись до Agent Hub
|
||||
← Back to Agent Hub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { profile, node, primary_city_room, system_prompts, public_profile, microdao_memberships } = dashboard;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen bg-gray-50 pb-12">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => navigate('/agent-hub')}
|
||||
className="text-blue-600 hover:text-blue-700 mb-4 flex items-center gap-2"
|
||||
>
|
||||
← Назад до Agent Hub
|
||||
← Back to Agent Hub
|
||||
</button>
|
||||
|
||||
{/* Agent header */}
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Avatar */}
|
||||
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold">
|
||||
{agent.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{agent.name}
|
||||
</h1>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${STATUS_COLORS[agent.status]}`} />
|
||||
<span className="text-sm text-gray-600">
|
||||
{STATUS_LABELS[agent.status]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{agent.description && (
|
||||
<p className="text-gray-600 mb-3">{agent.description}</p>
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="w-24 h-24 rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white text-4xl font-bold shadow-lg">
|
||||
{profile.avatar_url ? (
|
||||
<img src={profile.avatar_url} alt={profile.display_name} className="w-full h-full object-cover rounded-lg" />
|
||||
) : (
|
||||
profile.display_name.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>🤖</span>
|
||||
<span className="font-mono bg-gray-100 px-2 py-1 rounded">
|
||||
{agent.model}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{profile.display_name}</h1>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium uppercase tracking-wide ${
|
||||
profile.status === 'online' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{profile.status}
|
||||
</span>
|
||||
{profile.is_public && (
|
||||
<span className="px-2 py-1 rounded text-xs font-medium uppercase tracking-wide bg-purple-100 text-purple-800">
|
||||
Public Citizen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🤖</span>
|
||||
<span className="font-medium">{profile.kind}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>🏢</span>
|
||||
<span className="font-mono">{agent.microdao_id}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>🔧</span>
|
||||
<span>{agent.tools.length} інструментів</span>
|
||||
</div>
|
||||
{node && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🖥️</span>
|
||||
<span className="font-medium">{node.name}</span>
|
||||
<span className="text-xs text-gray-400">({node.status})</span>
|
||||
</div>
|
||||
)}
|
||||
{profile.primary_microdao_name && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🏢</span>
|
||||
<span className="font-medium">{profile.primary_microdao_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700"
|
||||
>
|
||||
🔄 Оновити
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate(`/messenger?agent=${agent.id}`)}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
💬 Чат
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex gap-1 border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab('metrics')}
|
||||
className={`
|
||||
px-6 py-3 font-medium transition-colors
|
||||
${activeTab === 'metrics'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
>
|
||||
📊 Метрики
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('context')}
|
||||
className={`
|
||||
px-6 py-3 font-medium transition-colors
|
||||
${activeTab === 'context'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
>
|
||||
🧠 Контекст
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`
|
||||
px-6 py-3 font-medium transition-colors
|
||||
${activeTab === 'settings'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
>
|
||||
⚙️ Налаштування
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('events')}
|
||||
className={`
|
||||
px-6 py-3 font-medium transition-colors
|
||||
${activeTab === 'events'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}
|
||||
`}
|
||||
>
|
||||
📜 Події
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{/* Main Content Grid */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{activeTab === 'metrics' && <AgentMetricsPanel agentId={agent.id} />}
|
||||
|
||||
{activeTab === 'context' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">🧠 Контекст агента</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Column: Identity & System */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
|
||||
{contextLoading ? (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-8 text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4" />
|
||||
<div className="text-gray-600">Завантаження контексту...</div>
|
||||
</div>
|
||||
) : context ? (
|
||||
{/* DAIS / System Prompts Block */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🧠 System Prompts (DAIS)</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Short-term memory */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">
|
||||
Короткострокова пам'ять ({context.short_term.length})
|
||||
</h4>
|
||||
{context.short_term.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{context.short_term.map((item) => (
|
||||
<div key={item.id} className="p-3 bg-blue-50 rounded text-sm">
|
||||
<div className="text-gray-900">{item.content}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{new Date(item.timestamp).toLocaleString('uk-UA')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{['core', 'safety', 'governance', 'tools'].map((kind) => {
|
||||
const prompt = system_prompts[kind as keyof typeof system_prompts];
|
||||
return (
|
||||
<div key={kind} className="border border-gray-100 rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-medium text-gray-700 uppercase text-xs tracking-wider">{kind} Prompt</span>
|
||||
{prompt && (
|
||||
<span className="text-xs text-gray-500">v{prompt.version} • {new Date(prompt.created_at).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-800 font-mono whitespace-pre-wrap max-h-32 overflow-y-auto">
|
||||
{prompt?.content || <span className="text-gray-400 italic">No prompt defined</span>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">Немає записів</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mid-term memory */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">
|
||||
Середньострокова пам'ять ({context.mid_term.length})
|
||||
</h4>
|
||||
{context.mid_term.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{context.mid_term.map((item) => (
|
||||
<div key={item.id} className="p-3 bg-purple-50 rounded text-sm">
|
||||
<div className="text-gray-900">{item.content}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{new Date(item.timestamp).toLocaleString('uk-UA')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">Немає записів</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Knowledge items */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h4 className="font-semibold text-gray-900 mb-3">
|
||||
База знань ({context.knowledge_items.length})
|
||||
</h4>
|
||||
{context.knowledge_items.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{context.knowledge_items.map((item) => (
|
||||
<div key={item.id} className="p-3 bg-green-50 rounded text-sm">
|
||||
<div className="text-gray-900">{item.content}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{new Date(item.timestamp).toLocaleString('uk-UA')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">Немає записів</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
|
||||
<div className="text-gray-500">Контекст недоступний</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Chat Block */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">💬 Agent Chat</h3>
|
||||
{primary_city_room ? (
|
||||
<AgentChatWidget roomId={primary_city_room.id} roomName={primary_city_room.name} />
|
||||
) : (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||
<div className="text-yellow-800 font-medium mb-2">No Primary Chat Room</div>
|
||||
<p className="text-sm text-yellow-600 mb-4">
|
||||
This agent is not assigned to a primary city room.
|
||||
</p>
|
||||
<button className="text-blue-600 hover:underline text-sm">
|
||||
Configure Rooms in MicroDAO
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right Column: Visibility & Roles */}
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* Visibility Card */}
|
||||
<VisibilityCard
|
||||
agentId={profile.agent_id}
|
||||
publicProfile={public_profile}
|
||||
onUpdate={refetch}
|
||||
/>
|
||||
|
||||
{/* MicroDAO Memberships */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🏢 MicroDAO Memberships</h3>
|
||||
{microdao_memberships.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{microdao_memberships.map((m) => (
|
||||
<div key={m.microdao_id} className="flex items-center justify-between p-3 border border-gray-100 rounded bg-gray-50 hover:bg-white transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{m.logo_url ? (
|
||||
<img src={m.logo_url} alt={m.microdao_name} className="w-8 h-8 rounded-full object-cover bg-gray-200" />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xs font-bold">
|
||||
{m.microdao_name?.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{m.microdao_name || 'Unknown DAO'}</div>
|
||||
<div className="text-xs text-gray-500">/{m.microdao_slug}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-xs font-medium px-2 py-1 rounded ${m.role === 'orchestrator' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800'}`}>
|
||||
{m.role || 'member'}
|
||||
</div>
|
||||
{m.is_core && <div className="text-[10px] text-gray-500 mt-1">Core Member</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm italic">Not a member of any MicroDAO</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
className="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-gray-400 hover:text-gray-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
+ Create MicroDAO (Orchestrator)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node Info */}
|
||||
{node && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">🖥️ Runtime Node</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Node Name</span>
|
||||
<span className="font-medium">{node.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">ID</span>
|
||||
<span className="font-mono text-xs">{node.node_id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Environment</span>
|
||||
<span>{node.environment || 'N/A'}</span>
|
||||
</div>
|
||||
{node.guardian_agent && (
|
||||
<div className="pt-2 mt-2 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-500 block mb-1">Guardian Agent</span>
|
||||
<span className="text-blue-600">{node.guardian_agent.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'settings' && (
|
||||
<AgentSettingsPanel agent={agent} onUpdate={refetch} />
|
||||
)}
|
||||
|
||||
{activeTab === 'events' && (
|
||||
<AgentEventsPanel agentId={agent.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MicroDaoWizard
|
||||
isOpen={isWizardOpen}
|
||||
onClose={() => setIsWizardOpen(false)}
|
||||
agentId={profile.agent_id}
|
||||
onSuccess={refetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,47 +3,26 @@
|
||||
* Single agent card for gallery view
|
||||
*/
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { AgentListItem } from '@/api/agents';
|
||||
import type { AgentSummary } from '@/types/agent-cabinet';
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: AgentListItem;
|
||||
agent: AgentSummary;
|
||||
}
|
||||
|
||||
const STATUS_COLORS = {
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
online: 'bg-green-500',
|
||||
offline: 'bg-gray-400',
|
||||
busy: 'bg-yellow-500',
|
||||
active: 'bg-green-500',
|
||||
idle: 'bg-yellow-500',
|
||||
offline: 'bg-gray-400',
|
||||
error: 'bg-red-500',
|
||||
};
|
||||
|
||||
const STATUS_LABELS = {
|
||||
active: 'Активний',
|
||||
idle: 'Очікує',
|
||||
offline: 'Офлайн',
|
||||
error: 'Помилка',
|
||||
};
|
||||
|
||||
const KIND_COLORS = {
|
||||
assistant: 'bg-blue-100 text-blue-800',
|
||||
node: 'bg-purple-100 text-purple-800',
|
||||
system: 'bg-gray-100 text-gray-800',
|
||||
guardian: 'bg-green-100 text-green-800',
|
||||
analyst: 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
|
||||
const KIND_LABELS = {
|
||||
assistant: 'Асистент',
|
||||
node: 'Нода',
|
||||
system: 'Система',
|
||||
guardian: 'Захисник',
|
||||
analyst: 'Аналітик',
|
||||
};
|
||||
|
||||
export function AgentCard({ agent }: AgentCardProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(`/agent/${agent.id}`);
|
||||
navigate(`/agents/${agent.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,59 +36,72 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
"
|
||||
>
|
||||
{/* Status indicator */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${STATUS_COLORS[agent.status]}`} />
|
||||
<span className="text-xs text-gray-500">
|
||||
{STATUS_LABELS[agent.status]}
|
||||
<div className="absolute top-4 right-4 flex gap-2">
|
||||
{agent.is_public && (
|
||||
<span className="text-[10px] font-bold uppercase tracking-wide bg-purple-100 text-purple-800 px-2 py-0.5 rounded-full">
|
||||
Public
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${STATUS_COLORS[agent.status] || 'bg-gray-300'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-2xl font-bold">
|
||||
{agent.name.charAt(0).toUpperCase()}
|
||||
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-2xl font-bold shadow-sm">
|
||||
{agent.avatar_url ? (
|
||||
<img src={agent.avatar_url} alt={agent.display_name} className="w-full h-full object-cover rounded-lg" />
|
||||
) : (
|
||||
agent.display_name.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{agent.name}
|
||||
<h3 className="text-lg font-bold text-gray-900 truncate pr-8">
|
||||
{agent.display_name}
|
||||
</h3>
|
||||
|
||||
{/* Kind badge */}
|
||||
<span className={`
|
||||
inline-block mt-1 px-2 py-1 rounded-full text-xs font-medium
|
||||
${KIND_COLORS[agent.kind]}
|
||||
`}>
|
||||
{KIND_LABELS[agent.kind]}
|
||||
<span className="inline-block mt-1 px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 uppercase tracking-wide">
|
||||
{agent.kind}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{agent.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{agent.description}
|
||||
</p>
|
||||
)}
|
||||
{/* Tagline/Description */}
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2 h-10">
|
||||
{agent.tagline || agent.title || 'No description provided.'}
|
||||
</p>
|
||||
|
||||
{/* Model */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-500">Модель:</span>
|
||||
<span className="font-mono text-gray-900 bg-gray-100 px-2 py-1 rounded">
|
||||
{agent.model}
|
||||
</span>
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="space-y-2 text-xs text-gray-500">
|
||||
{agent.node_label && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🖥️</span>
|
||||
<span>Node</span>
|
||||
</div>
|
||||
<span className="font-medium text-gray-700">{agent.node_label}</span>
|
||||
</div>
|
||||
)}
|
||||
{agent.primary_microdao_name && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🏢</span>
|
||||
<span>MicroDAO</span>
|
||||
</div>
|
||||
<span className="font-medium text-gray-700 truncate max-w-[120px]">
|
||||
{agent.primary_microdao_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last active */}
|
||||
{agent.last_active_at && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 text-xs text-gray-500">
|
||||
Остання активність: {new Date(agent.last_active_at).toLocaleString('uk-UA')}
|
||||
</div>
|
||||
)}
|
||||
{/* Footer */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex justify-between items-center text-xs text-blue-600 font-medium">
|
||||
<span>Open Cabinet →</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Grid view of agent cards
|
||||
*/
|
||||
import { AgentCard } from './AgentCard';
|
||||
import type { AgentListItem } from '@/api/agents';
|
||||
import type { AgentSummary } from '@/types/agent-cabinet';
|
||||
|
||||
interface AgentGalleryProps {
|
||||
agents: AgentListItem[];
|
||||
agents: AgentSummary[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function AgentGallery({ agents, loading, error }: AgentGalleryProps) {
|
||||
@@ -40,8 +40,8 @@ export function AgentGallery({ agents, loading, error }: AgentGalleryProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-8 text-center">
|
||||
<div className="text-red-600 mb-2">❌ Помилка завантаження агентів</div>
|
||||
<div className="text-sm text-red-500">{error.message}</div>
|
||||
<div className="text-red-600 mb-2">❌ Failed to load agents</div>
|
||||
<div className="text-sm text-red-500">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -52,10 +52,10 @@ export function AgentGallery({ agents, loading, error }: AgentGalleryProps) {
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-12 text-center">
|
||||
<div className="text-6xl mb-4">🤖</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Немає агентів
|
||||
No Agents Found
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Агенти з'являться тут після їх створення
|
||||
Try adjusting your filters or create a new agent.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,33 +2,52 @@
|
||||
* AgentHubPage Component
|
||||
* Main page for Agent Hub — browse all agents
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useAgents } from './hooks/useAgents';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAgentsV2 } from './hooks/useAgentsV2';
|
||||
import { AgentGallery } from './AgentGallery';
|
||||
import { AgentCreateDialog } from './AgentCreateDialog';
|
||||
import { useActor } from '@/store/authStore';
|
||||
import { fetchCityNodes, fetchCityMicroDAOs } from '@/api/city';
|
||||
|
||||
export function AgentHubPage() {
|
||||
const actor = useActor();
|
||||
const [filters, setFilters] = useState({
|
||||
node_id: '',
|
||||
microdao_id: '',
|
||||
kind: '',
|
||||
is_public: undefined as boolean | undefined
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedMicroDao, setSelectedMicroDao] = useState<string | undefined>(undefined);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
|
||||
const { agents, loading, error, refetch } = useAgents(selectedMicroDao);
|
||||
// Data for filters
|
||||
const [nodes, setNodes] = useState<{node_id: string, name: string}[]>([]);
|
||||
const [microdaos, setMicrodaos] = useState<{id: string, name: string}[]>([]);
|
||||
|
||||
const { agents, loading, error, refetch } = useAgentsV2(filters);
|
||||
|
||||
useEffect(() => {
|
||||
// Load filter options
|
||||
fetchCityNodes().then(res => setNodes(res.items.map(n => ({ node_id: n.node_id, name: n.name }))));
|
||||
fetchCityMicroDAOs().then(res => setMicrodaos(res.items.map(m => ({ id: m.id, name: m.name }))));
|
||||
}, []);
|
||||
|
||||
const handleCreateSuccess = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
// Filter agents by search query
|
||||
// Filter agents by search query (client-side search for now)
|
||||
const filteredAgents = agents.filter((agent) => {
|
||||
const matchesSearch =
|
||||
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
return matchesSearch;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return (
|
||||
agent.display_name.toLowerCase().includes(q) ||
|
||||
agent.title?.toLowerCase().includes(q) ||
|
||||
agent.tagline?.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const handleFilterChange = (key: string, value: any) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value || undefined }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
@@ -37,59 +56,82 @@ export function AgentHubPage() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
🤖 Agent Hub
|
||||
🤖 Agent Console
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Керуйте агентами вашого MicroDAO
|
||||
Manage and monitor all agents in the city
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
➕ Створити агента
|
||||
<span>➕</span> Create Agent
|
||||
</button>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
🔄 Оновити
|
||||
<span>🔄</span> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1">
|
||||
<div className="md:col-span-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Пошук агентів..."
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="
|
||||
w-full px-4 py-2 border border-gray-300 rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MicroDAO filter */}
|
||||
<div className="sm:w-64">
|
||||
{/* Node Filter */}
|
||||
<div>
|
||||
<select
|
||||
value={selectedMicroDao || ''}
|
||||
onChange={(e) => setSelectedMicroDao(e.target.value || undefined)}
|
||||
className="
|
||||
w-full px-4 py-2 border border-gray-300 rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
"
|
||||
value={filters.node_id || ''}
|
||||
onChange={(e) => handleFilterChange('node_id', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Всі MicroDAO</option>
|
||||
<option value="microdao:daarion">DAARION</option>
|
||||
<option value="microdao:7">MicroDAO #7</option>
|
||||
<option value="microdao:greenfood">GreenFood</option>
|
||||
<option value="">All Nodes</option>
|
||||
{nodes.map(node => (
|
||||
<option key={node.node_id} value={node.node_id}>{node.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* MicroDAO Filter */}
|
||||
<div>
|
||||
<select
|
||||
value={filters.microdao_id || ''}
|
||||
onChange={(e) => handleFilterChange('microdao_id', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">All MicroDAOs</option>
|
||||
{microdaos.map(dao => (
|
||||
<option key={dao.id} value={dao.id}>{dao.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Kind Filter */}
|
||||
<div>
|
||||
<select
|
||||
value={filters.kind || ''}
|
||||
onChange={(e) => handleFilterChange('kind', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">All Kinds</option>
|
||||
<option value="assistant">Assistant</option>
|
||||
<option value="node_guardian">Node Guardian</option>
|
||||
<option value="node_steward">Node Steward</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,21 +142,21 @@ export function AgentHubPage() {
|
||||
<div className="text-2xl font-bold text-blue-900">
|
||||
{filteredAgents.length}
|
||||
</div>
|
||||
<div className="text-sm text-blue-600">Знайдено агентів</div>
|
||||
<div className="text-sm text-blue-600">Total Agents</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-green-900">
|
||||
{filteredAgents.filter(a => a.status === 'active').length}
|
||||
{filteredAgents.filter(a => a.status === 'online' || a.status === 'active').length}
|
||||
</div>
|
||||
<div className="text-sm text-green-600">Активних</div>
|
||||
<div className="text-sm text-green-600">Online / Active</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-purple-900">
|
||||
{actor?.microdao_ids.length || 0}
|
||||
{filteredAgents.filter(a => a.is_public).length}
|
||||
</div>
|
||||
<div className="text-sm text-purple-600">Ваших MicroDAO</div>
|
||||
<div className="text-sm text-purple-600">Public Citizens</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
167
src/features/agentHub/components/AgentChatWidget.tsx
Normal file
167
src/features/agentHub/components/AgentChatWidget.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
getCityRoom,
|
||||
sendCityRoomMessage,
|
||||
joinCityRoom,
|
||||
leaveCityRoom,
|
||||
type CityRoomDetail,
|
||||
type CityRoomMessage
|
||||
} from '@/api/cityRooms';
|
||||
import { WebSocketClient } from '@/lib/ws';
|
||||
|
||||
interface AgentChatWidgetProps {
|
||||
roomId: string;
|
||||
roomName?: string;
|
||||
}
|
||||
|
||||
export function AgentChatWidget({ roomId, roomName }: AgentChatWidgetProps) {
|
||||
const [room, setRoom] = useState<CityRoomDetail | null>(null);
|
||||
const [messages, setMessages] = useState<CityRoomMessage[]>([]);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocketClient | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomId) return;
|
||||
|
||||
loadRoom();
|
||||
joinRoom();
|
||||
|
||||
// WebSocket connection
|
||||
const wsUrl = `${import.meta.env.VITE_WS_URL || 'ws://localhost:8000'}/ws/city/rooms/${roomId}`;
|
||||
const ws = new WebSocketClient({ url: wsUrl });
|
||||
|
||||
ws.onMessage((data: any) => {
|
||||
if (data.event === 'city.room.message') {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
}
|
||||
});
|
||||
|
||||
ws.connect();
|
||||
wsRef.current = ws;
|
||||
|
||||
return () => {
|
||||
leaveRoom();
|
||||
ws.disconnect();
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const loadRoom = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getCityRoom(roomId);
|
||||
setRoom(data);
|
||||
setMessages(data.messages);
|
||||
} catch (error) {
|
||||
console.error('Failed to load room:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const joinRoom = async () => {
|
||||
try {
|
||||
await joinCityRoom(roomId);
|
||||
} catch (error) {
|
||||
console.error('Failed to join room:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const leaveRoom = async () => {
|
||||
try {
|
||||
await leaveCityRoom(roomId);
|
||||
} catch (error) {
|
||||
console.error('Failed to leave room:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputText.trim()) return;
|
||||
|
||||
try {
|
||||
await sendCityRoomMessage(roomId, { text: inputText });
|
||||
setInputText('');
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-center py-8 text-gray-500">Connecting to chat...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[500px] bg-white border border-gray-200 rounded-lg shadow-sm">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b bg-gray-50 rounded-t-lg flex justify-between items-center">
|
||||
<h3 className="font-semibold text-gray-900">{room?.name || roomName || 'Agent Chat'}</h3>
|
||||
<div className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-white">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-gray-400 text-sm py-8">
|
||||
No messages yet. Start the conversation!
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div key={message.id} className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm text-gray-900">{message.username}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 p-2 rounded-lg text-gray-800 text-sm">
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-3 border-t bg-gray-50 rounded-b-lg">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputText.trim()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-300 text-sm font-medium"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
286
src/features/agentHub/components/MicroDaoWizard.tsx
Normal file
286
src/features/agentHub/components/MicroDaoWizard.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { useState } from 'react';
|
||||
import { createMicroDaoFromAgent } from '@/api/microdaoWizard';
|
||||
|
||||
interface MicroDaoWizardProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
agentId: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function MicroDaoWizard({ isOpen, onClose, agentId, onSuccess }: MicroDaoWizardProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
visibility: 'public' as 'public' | 'confidential',
|
||||
district: '',
|
||||
create_rooms: {
|
||||
primary_lobby: true,
|
||||
governance: true,
|
||||
crew_team: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleNext = () => setStep(s => s + 1);
|
||||
const handleBack = () => setStep(s => s - 1);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await createMicroDaoFromAgent({
|
||||
agent_id: agentId,
|
||||
...formData
|
||||
});
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError(err.message || 'Failed to create MicroDAO');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-generate slug from name
|
||||
const handleNameChange = (name: string) => {
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
setFormData(prev => ({ ...prev, name, slug: prev.slug ? prev.slug : slug }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl max-w-lg w-full shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<h2 className="text-lg font-bold text-gray-900">Create MicroDAO</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">✕</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
{/* Steps Progress */}
|
||||
<div className="flex items-center mb-8 text-sm">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="flex items-center">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold ${
|
||||
step === i ? 'bg-blue-600 text-white' :
|
||||
step > i ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{step > i ? '✓' : i}
|
||||
</div>
|
||||
{i < 3 && <div className={`w-12 h-1 ${step > i ? 'bg-green-500' : 'bg-gray-200'} mx-2`} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 text-red-700 text-sm rounded-lg border border-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Basic Info */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">MicroDAO Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="e.g. Solar Punks"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Unique Slug</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm">
|
||||
/microdao/
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({...formData, slug: e.target.value})}
|
||||
className="flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-r-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="solar-punks"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
rows={3}
|
||||
placeholder="What is this community about?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Visibility */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">Visibility</label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => setFormData({...formData, visibility: 'public'})}
|
||||
className={`p-4 border rounded-xl text-left transition-all ${
|
||||
formData.visibility === 'public'
|
||||
? 'border-blue-500 bg-blue-50 ring-1 ring-blue-500'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-gray-900 mb-1">Public</div>
|
||||
<div className="text-xs text-gray-500">Visible to everyone in the City Directory.</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFormData({...formData, visibility: 'confidential'})}
|
||||
className={`p-4 border rounded-xl text-left transition-all ${
|
||||
formData.visibility === 'confidential'
|
||||
? 'border-purple-500 bg-purple-50 ring-1 ring-purple-500'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-gray-900 mb-1">Confidential</div>
|
||||
<div className="text-xs text-gray-500">Hidden from directory. Invite only.</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">District / Platform (Optional)</label>
|
||||
<select
|
||||
value={formData.district}
|
||||
onChange={(e) => setFormData({...formData, district: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white"
|
||||
>
|
||||
<option value="">None (Independent)</option>
|
||||
<option value="core">Core Infrastructure</option>
|
||||
<option value="green">Green Economy</option>
|
||||
<option value="tech">Tech Labs</option>
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500">Assigning a district may require approval.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Rooms */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||
<h3 className="font-bold text-blue-900 mb-2">Communication Channels</h3>
|
||||
<p className="text-sm text-blue-800 mb-4">
|
||||
Select which rooms to create automatically. These will be Matrix-powered chat rooms.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 bg-white p-3 rounded border border-blue-100">
|
||||
<label className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.create_rooms.primary_lobby}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
create_rooms: {...formData.create_rooms, primary_lobby: e.target.checked}
|
||||
})}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Primary Lobby</div>
|
||||
<div className="text-xs text-gray-500">Public general chat for the community</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.create_rooms.governance}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
create_rooms: {...formData.create_rooms, governance: e.target.checked}
|
||||
})}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Governance Hall</div>
|
||||
<div className="text-xs text-gray-500">For voting and formal proposals</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.create_rooms.crew_team}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
create_rooms: {...formData.create_rooms, crew_team: e.target.checked}
|
||||
})}
|
||||
className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Crew Team (Internal)</div>
|
||||
<div className="text-xs text-gray-500">Private workspace for agents and core team</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex justify-between">
|
||||
{step > 1 ? (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="px-4 py-2 text-gray-600 font-medium hover:text-gray-900"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{step < 3 ? (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={!formData.name || !formData.slug}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next Step
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create MicroDAO'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
151
src/features/agentHub/components/VisibilityCard.tsx
Normal file
151
src/features/agentHub/components/VisibilityCard.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useState } from 'react';
|
||||
import { updateAgentVisibility } from '@/api/agents';
|
||||
import type { AgentDetailDashboard } from '@/types/agent-cabinet';
|
||||
|
||||
interface VisibilityCardProps {
|
||||
agentId: string;
|
||||
publicProfile: AgentDetailDashboard['public_profile'];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export function VisibilityCard({ agentId, publicProfile, onUpdate }: VisibilityCardProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
is_public: publicProfile?.is_public || false,
|
||||
public_slug: publicProfile?.public_slug || '',
|
||||
public_title: publicProfile?.public_title || '',
|
||||
public_tagline: publicProfile?.public_tagline || ''
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateAgentVisibility(agentId, {
|
||||
is_public: formData.is_public,
|
||||
public_slug: formData.public_slug || null,
|
||||
public_title: formData.public_title || null,
|
||||
public_tagline: formData.public_tagline || null
|
||||
});
|
||||
setIsEditing(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
console.error('Failed to update visibility:', error);
|
||||
alert('Failed to update visibility');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const previewUrl = formData.public_slug ? `/citizens/${formData.public_slug}` : '#';
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">🌐 Visibility & Public Profile</h3>
|
||||
<button
|
||||
onClick={() => isEditing ? handleSave() : setIsEditing(true)}
|
||||
disabled={saving}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
isEditing
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-400'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Saving...' : isEditing ? 'Save Changes' : 'Edit Settings'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Toggle Public */}
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Public Citizen Profile</div>
|
||||
<div className="text-sm text-gray-500">Make this agent visible in the City Directory</div>
|
||||
</div>
|
||||
<div className="relative inline-block w-12 h-6 transition duration-200 ease-in-out">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle-public"
|
||||
className="peer absolute w-12 h-6 opacity-0 z-10 cursor-pointer"
|
||||
checked={formData.is_public}
|
||||
disabled={!isEditing}
|
||||
onChange={(e) => setFormData({ ...formData, is_public: e.target.checked })}
|
||||
/>
|
||||
<label
|
||||
htmlFor="toggle-public"
|
||||
className={`block h-6 rounded-full cursor-pointer transition-colors ${
|
||||
formData.is_public ? 'bg-green-500' : 'bg-gray-300'
|
||||
}`}
|
||||
></label>
|
||||
<div
|
||||
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${
|
||||
formData.is_public ? 'translate-x-6' : 'translate-x-0'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Public Slug</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm">
|
||||
/citizens/
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.public_slug}
|
||||
onChange={(e) => setFormData({ ...formData, public_slug: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
placeholder="agent-name"
|
||||
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">Unique identifier for public URL</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Public Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.public_title}
|
||||
onChange={(e) => setFormData({ ...formData, public_title: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
placeholder="e.g. City Guide"
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Public Tagline</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.public_tagline}
|
||||
onChange={(e) => setFormData({ ...formData, public_tagline: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
placeholder="Short description for the card..."
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Link */}
|
||||
{formData.is_public && formData.public_slug && (
|
||||
<div className="pt-4 border-t border-gray-100 flex justify-end">
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
|
||||
>
|
||||
View Public Profile ↗
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
31
src/features/agentHub/hooks/useAgentDashboard.ts
Normal file
31
src/features/agentHub/hooks/useAgentDashboard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getAgentDashboard } from '@/api/agents';
|
||||
import { AgentDetailDashboard } from '@/types/agent-cabinet';
|
||||
|
||||
export function useAgentDashboard(agentId: string) {
|
||||
const [dashboard, setDashboard] = useState<AgentDetailDashboard | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchDashboard = useCallback(async () => {
|
||||
if (!agentId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAgentDashboard(agentId);
|
||||
setDashboard(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to load agent dashboard');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboard();
|
||||
}, [fetchDashboard]);
|
||||
|
||||
return { dashboard, loading, error, refetch: fetchDashboard };
|
||||
}
|
||||
|
||||
30
src/features/agentHub/hooks/useAgentsV2.ts
Normal file
30
src/features/agentHub/hooks/useAgentsV2.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getAgentList } from '@/api/agents';
|
||||
import { AgentSummary } from '@/types/agent-cabinet';
|
||||
|
||||
export function useAgentsV2(filters: Record<string, any> = {}) {
|
||||
const [agents, setAgents] = useState<AgentSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchAgents = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAgentList(filters);
|
||||
setAgents(data.items);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Failed to load agents');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [JSON.stringify(filters)]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
return { agents, loading, error, refetch: fetchAgents };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type CityRoomMessage
|
||||
} from '../../../api/cityRooms';
|
||||
import { WebSocketClient } from '../../../lib/ws';
|
||||
import { RoomBrandHeader } from '../../microdao/components/RoomBrandHeader';
|
||||
|
||||
export function CityRoomView() {
|
||||
const { roomId } = useParams<{ roomId: string }>();
|
||||
@@ -124,30 +125,22 @@ export function CityRoomView() {
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/city/rooms')}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
← Назад
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{room.name}</h1>
|
||||
{room.description && (
|
||||
<p className="text-sm text-gray-600">{room.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-1 text-sm bg-green-100 text-green-800 px-3 py-1 rounded-full">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
{room.members_online} онлайн
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RoomBrandHeader
|
||||
name={room.name}
|
||||
description={room.description}
|
||||
bannerUrl={room.banner_url}
|
||||
logoUrl={room.logo_url}
|
||||
microdaoLogoUrl={room.microdao_logo_url}
|
||||
microdaoName={room.microdao_name}
|
||||
membersCount={room.members_online}
|
||||
>
|
||||
<button
|
||||
onClick={() => navigate('/city/rooms')}
|
||||
className="text-white/80 hover:text-white flex items-center gap-2 bg-black/20 px-3 py-1.5 rounded-lg backdrop-blur-sm hover:bg-black/30 transition-all"
|
||||
>
|
||||
← Назад
|
||||
</button>
|
||||
</RoomBrandHeader>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { getMicrodao, getMembers, getTreasury, type MicrodaoRead, type MicrodaoMember, type TreasuryItem } from '@/api/microdao';
|
||||
import { getMicrodao, getMembers, getTreasury, uploadAsset, updateMicrodaoBranding, type MicrodaoRead, type MicrodaoMember, type TreasuryItem } from '@/api/microdao';
|
||||
import { getAgents, type AgentListItem } from '@/api/agents';
|
||||
import { MicrodaoHero } from './components/MicrodaoHero';
|
||||
import { MicrodaoBrandBadge } from './components/MicrodaoBrandBadge';
|
||||
|
||||
type TabType = 'overview' | 'members' | 'agents' | 'treasury';
|
||||
type TabType = 'overview' | 'members' | 'agents' | 'treasury' | 'settings';
|
||||
|
||||
export function MicrodaoConsolePage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
@@ -62,6 +64,26 @@ export function MicrodaoConsolePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File | undefined, type: string) => {
|
||||
if (!file || !microdao) return;
|
||||
try {
|
||||
// Optimistic UI update could be done here, but better wait for server
|
||||
const { processed_url } = await uploadAsset(file, type);
|
||||
|
||||
// Update branding
|
||||
const updated = await updateMicrodaoBranding(
|
||||
microdao.slug,
|
||||
type === 'microdao_logo' ? processed_url : undefined,
|
||||
type === 'microdao_banner' ? processed_url : undefined
|
||||
);
|
||||
|
||||
setMicrodao(prev => prev ? { ...prev, ...updated } : null);
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err);
|
||||
alert('Upload failed');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
@@ -95,81 +117,82 @@ export function MicrodaoConsolePage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
{/* Hero Header */}
|
||||
<MicrodaoHero
|
||||
name={microdao.name}
|
||||
tagline={microdao.description}
|
||||
logoUrl={microdao.logo_url}
|
||||
bannerUrl={microdao.banner_url}
|
||||
>
|
||||
<button
|
||||
onClick={() => navigate('/microdao')}
|
||||
className="px-4 py-2 bg-white/10 backdrop-blur-md border border-white/20 text-white rounded-lg hover:bg-white/20 transition-colors text-sm"
|
||||
>
|
||||
← Список
|
||||
</button>
|
||||
<Link
|
||||
to={`/dao/${microdao.slug}-governance`}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 shadow-md text-sm font-medium"
|
||||
>
|
||||
<span>🗳️</span>
|
||||
<span>Governance</span>
|
||||
</Link>
|
||||
</MicrodaoHero>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<button
|
||||
onClick={() => navigate('/microdao')}
|
||||
className="text-blue-600 hover:text-blue-700 mb-4 flex items-center gap-2"
|
||||
>
|
||||
← Назад до списку
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{microdao.name}
|
||||
</h1>
|
||||
{microdao.description && (
|
||||
<p className="text-gray-600 mt-1">{microdao.description}</p>
|
||||
)}
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
<span className="font-mono">{microdao.slug}</span> · {microdao.external_id}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
to={`/dao/${microdao.slug}-governance`}
|
||||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition flex items-center gap-2 shadow-md"
|
||||
>
|
||||
<span className="text-xl">🗳️</span>
|
||||
<span className="font-semibold">DAO Governance</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b border-gray-200 mt-6">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex gap-1 -mb-px pt-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'overview'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
📊 Огляд
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('members')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'members'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
👥 Учасники ({members.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('agents')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'agents'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
🤖 Агенти ({agents.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('treasury')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'treasury'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
💰 Казна
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'settings'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
⚙️ Налаштування
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,6 +321,52 @@ export function MicrodaoConsolePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Tab */}
|
||||
{activeTab === 'settings' && microdao && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 space-y-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-6">Брендинг</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Logo Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Логотип (256x256)</label>
|
||||
<div className="flex items-center gap-6">
|
||||
<MicrodaoBrandBadge name={microdao.name} logoUrl={microdao.logo_url} size="xl" />
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleUpload(e.target.files?.[0], 'microdao_logo')}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-2">PNG, JPG, SVG. Макс 5MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banner Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Банер</label>
|
||||
<div className="relative w-full h-32 bg-gray-100 rounded-lg overflow-hidden mb-3 border border-gray-200 group">
|
||||
{microdao.banner_url ? (
|
||||
<img src={microdao.banner_url} className="w-full h-full object-cover" alt="Banner" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">Немає банера</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleUpload(e.target.files?.[0], 'microdao_banner')}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMicrodaos } from './hooks/useMicrodaos';
|
||||
import { createMicrodao, type MicrodaoCreate } from '@/api/microdao';
|
||||
import { MicrodaoBrandBadge } from './components/MicrodaoBrandBadge';
|
||||
|
||||
export function MicrodaoListPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -120,24 +121,42 @@ export function MicrodaoListPage() {
|
||||
<div
|
||||
key={dao.id}
|
||||
onClick={() => navigate(`/microdao/${dao.slug}`)}
|
||||
className="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-lg hover:border-blue-500 transition-all cursor-pointer"
|
||||
className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg hover:border-blue-500 transition-all cursor-pointer group relative"
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{dao.name}
|
||||
</h3>
|
||||
|
||||
{dao.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{dao.description}
|
||||
</p>
|
||||
{/* Banner Background */}
|
||||
{dao.banner_url && (
|
||||
<div
|
||||
className="absolute inset-0 h-32 bg-cover bg-center opacity-10 group-hover:opacity-20 transition-opacity"
|
||||
style={{ backgroundImage: `url(${dao.banner_url})` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<div>
|
||||
👥 {dao.member_count || 0} учасників
|
||||
|
||||
<div className="p-6 relative z-10">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<MicrodaoBrandBadge name={dao.name} logoUrl={dao.logo_url} size="md" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{dao.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">@{dao.slug}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
🤖 {dao.agent_count || 0} агентів
|
||||
|
||||
{dao.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{dao.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 mt-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>👥</span> {dao.member_count || 0}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🤖</span> {dao.agent_count || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
src/features/microdao/components/MicrodaoBrandBadge.tsx
Normal file
47
src/features/microdao/components/MicrodaoBrandBadge.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Building2 } from 'lucide-react';
|
||||
|
||||
interface MicrodaoBrandBadgeProps {
|
||||
logoUrl?: string | null;
|
||||
name: string;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MicrodaoBrandBadge: React.FC<MicrodaoBrandBadgeProps> = ({
|
||||
logoUrl,
|
||||
name,
|
||||
size = 'md',
|
||||
className = ''
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-8 h-8',
|
||||
md: 'w-12 h-12',
|
||||
lg: 'w-16 h-16',
|
||||
xl: 'w-24 h-24'
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: 16,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
xl: 48
|
||||
};
|
||||
|
||||
if (logoUrl) {
|
||||
return (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
className={`rounded-full object-cover bg-gray-100 shadow-sm border border-gray-200/50 ${sizeClasses[size]} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-full bg-gradient-to-br from-blue-500/10 to-purple-500/10 flex items-center justify-center border border-gray-200 shadow-sm ${sizeClasses[size]} ${className}`}>
|
||||
<Building2 size={iconSizes[size]} className="text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
71
src/features/microdao/components/MicrodaoHero.tsx
Normal file
71
src/features/microdao/components/MicrodaoHero.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { MicrodaoBrandBadge } from './MicrodaoBrandBadge';
|
||||
|
||||
interface MicrodaoHeroProps {
|
||||
bannerUrl?: string | null;
|
||||
logoUrl?: string | null;
|
||||
name: string;
|
||||
tagline?: string | null;
|
||||
children?: React.ReactNode; // For action buttons etc.
|
||||
}
|
||||
|
||||
export const MicrodaoHero: React.FC<MicrodaoHeroProps> = ({
|
||||
bannerUrl,
|
||||
logoUrl,
|
||||
name,
|
||||
tagline,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative w-full h-48 md:h-64 lg:h-80 bg-gray-900 overflow-hidden group">
|
||||
{/* Background / Banner */}
|
||||
{bannerUrl ? (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-center transition-transform duration-700 group-hover:scale-105"
|
||||
style={{ backgroundImage: `url(${bannerUrl})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 w-full h-full bg-gradient-to-r from-slate-900 via-purple-900 to-slate-900" />
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-[2px]" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 w-full p-6 md:p-8">
|
||||
<div className="container mx-auto flex flex-col md:flex-row items-end md:items-end gap-6">
|
||||
{/* Logo */}
|
||||
<div className="relative -mb-2 md:mb-0 shrink-0">
|
||||
<MicrodaoBrandBadge
|
||||
logoUrl={logoUrl}
|
||||
name={name}
|
||||
size="xl"
|
||||
className="ring-4 ring-black/20 shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1 mb-1">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-white drop-shadow-lg tracking-tight">
|
||||
{name}
|
||||
</h1>
|
||||
{tagline && (
|
||||
<p className="text-gray-200 text-lg mt-1 max-w-2xl drop-shadow-md font-light">
|
||||
{tagline}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{children && (
|
||||
<div className="flex items-center gap-3 mt-4 md:mt-0">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
90
src/features/microdao/components/RoomBrandHeader.tsx
Normal file
90
src/features/microdao/components/RoomBrandHeader.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { MicrodaoBrandBadge } from './MicrodaoBrandBadge';
|
||||
import { Hash, Users, Info } from 'lucide-react';
|
||||
|
||||
interface RoomBrandHeaderProps {
|
||||
bannerUrl?: string | null;
|
||||
logoUrl?: string | null; // Room logo
|
||||
microdaoLogoUrl?: string | null; // Fallback to MicroDAO logo
|
||||
name: string;
|
||||
description?: string | null;
|
||||
microdaoName?: string;
|
||||
membersCount?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RoomBrandHeader: React.FC<RoomBrandHeaderProps> = ({
|
||||
bannerUrl,
|
||||
logoUrl,
|
||||
microdaoLogoUrl,
|
||||
name,
|
||||
description,
|
||||
microdaoName,
|
||||
membersCount,
|
||||
children
|
||||
}) => {
|
||||
// Use room logo if available, else MicroDAO logo
|
||||
const displayLogo = logoUrl || microdaoLogoUrl;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-32 md:h-40 bg-gray-800 overflow-hidden rounded-t-xl shrink-0">
|
||||
{/* Background / Banner */}
|
||||
{bannerUrl ? (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${bannerUrl})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 w-full h-full bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800" />
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
|
||||
|
||||
{/* Top Actions (Children) */}
|
||||
{children && (
|
||||
<div className="absolute top-0 left-0 w-full p-4 z-10">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 w-full p-4 md:p-6 flex items-end gap-4">
|
||||
<div className="shrink-0">
|
||||
<MicrodaoBrandBadge
|
||||
logoUrl={displayLogo}
|
||||
name={name}
|
||||
size="lg"
|
||||
className="ring-2 ring-black/10 shadow-lg bg-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 mb-0.5">
|
||||
<div className="flex items-center gap-2 text-gray-300 text-xs uppercase tracking-wider font-medium mb-0.5">
|
||||
{microdaoName && <span>{microdaoName}</span>}
|
||||
{membersCount !== undefined && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
{membersCount} online
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl md:text-2xl font-bold text-white flex items-center gap-2 truncate">
|
||||
<Hash className="text-gray-400 w-5 h-5 md:w-6 md:h-6 shrink-0" />
|
||||
{name}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="text-gray-300 text-sm truncate max-w-xl opacity-90">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,114 +1,34 @@
|
||||
/**
|
||||
* Connect Node Page - Спрощений UI для підключення ноди
|
||||
* Для звичайних користувачів (без термінала)
|
||||
* Connect Node Page - Інструкції з підключення ноди
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Download, Copy, CheckCircle, Monitor, Cpu, HardDrive } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Download, Copy, CheckCircle, Monitor, Cpu, HardDrive, Terminal, ExternalLink, AlertCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { apiGet } from '../api/client';
|
||||
|
||||
export default function ConnectNodePage() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [selectedOS, setSelectedOS] = useState<'macos' | 'linux' | 'windows'>('macos');
|
||||
const [instructions, setInstructions] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const registryUrl = 'http://localhost:9205'; // TODO: змінити на production URL
|
||||
useEffect(() => {
|
||||
const fetchInstructions = async () => {
|
||||
try {
|
||||
const response = await apiGet<{ content: string }>('/public/nodes/join/instructions');
|
||||
if (response.content) {
|
||||
setInstructions(response.content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch instructions:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Інструкції для різних ОС
|
||||
const instructions = {
|
||||
macos: {
|
||||
title: '🍎 macOS',
|
||||
steps: [
|
||||
{
|
||||
title: '1. Завантажити Bootstrap Agent',
|
||||
description: 'Скачайте скрипт автоматичної реєстрації',
|
||||
action: 'download',
|
||||
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
|
||||
},
|
||||
{
|
||||
title: '2. Встановити залежності',
|
||||
description: 'Встановіть необхідні Python бібліотеки',
|
||||
code: 'pip3 install --user requests psutil',
|
||||
},
|
||||
{
|
||||
title: '3. Запустити Bootstrap Agent',
|
||||
description: 'Запустіть агент для автоматичної реєстрації',
|
||||
code: `export NODE_REGISTRY_URL="${registryUrl}"
|
||||
export NODE_ROLE="worker"
|
||||
python3 node_bootstrap.py`,
|
||||
},
|
||||
],
|
||||
},
|
||||
linux: {
|
||||
title: '🐧 Linux',
|
||||
steps: [
|
||||
{
|
||||
title: '1. Завантажити Bootstrap Agent',
|
||||
description: 'Скачайте скрипт автоматичної реєстрації',
|
||||
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
|
||||
},
|
||||
{
|
||||
title: '2. Встановити залежності',
|
||||
description: 'Встановіть необхідні Python бібліотеки',
|
||||
code: 'pip3 install requests psutil',
|
||||
},
|
||||
{
|
||||
title: '3. Запустити Bootstrap Agent',
|
||||
description: 'Запустіть агент для автоматичної реєстрації',
|
||||
code: `export NODE_REGISTRY_URL="${registryUrl}"
|
||||
export NODE_ROLE="worker"
|
||||
python3 node_bootstrap.py`,
|
||||
},
|
||||
{
|
||||
title: '4. (Опціонально) Додати як systemd service',
|
||||
description: 'Для автоматичного запуску при перезавантаженні',
|
||||
code: `sudo tee /etc/systemd/system/node-bootstrap.service << EOF
|
||||
[Unit]
|
||||
Description=DAGI Node Bootstrap
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment="NODE_REGISTRY_URL=${registryUrl}"
|
||||
Environment="NODE_ROLE=worker"
|
||||
ExecStart=/usr/bin/python3 /opt/dagi/node_bootstrap.py
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl enable node-bootstrap
|
||||
sudo systemctl start node-bootstrap`,
|
||||
},
|
||||
],
|
||||
},
|
||||
windows: {
|
||||
title: '🪟 Windows',
|
||||
steps: [
|
||||
{
|
||||
title: '1. Завантажити Bootstrap Agent',
|
||||
description: 'Скачайте скрипт автоматичної реєстрації',
|
||||
code: 'curl -O http://localhost:9205/bootstrap/node_bootstrap.py',
|
||||
},
|
||||
{
|
||||
title: '2. Встановити Python',
|
||||
description: 'Завантажте Python 3.9+ з python.org',
|
||||
link: 'https://www.python.org/downloads/',
|
||||
},
|
||||
{
|
||||
title: '3. Встановити залежності',
|
||||
description: 'Відкрийте PowerShell та виконайте',
|
||||
code: 'pip install requests psutil',
|
||||
},
|
||||
{
|
||||
title: '4. Запустити Bootstrap Agent',
|
||||
description: 'Запустіть агент для автоматичної реєстрації',
|
||||
code: `$env:NODE_REGISTRY_URL="${registryUrl}"
|
||||
$env:NODE_ROLE="worker"
|
||||
python node_bootstrap.py`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
fetchInstructions();
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
@@ -116,182 +36,161 @@ python node_bootstrap.py`,
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const currentInstructions = instructions[selectedOS];
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-purple-950/20 to-slate-950 text-white p-6">
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
🔌 Підключити Ноду до DAGI
|
||||
<Link to="/nodes" className="text-blue-600 hover:underline mb-4 inline-block">← Назад до списку нод</Link>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-900">
|
||||
🔌 Підключити Ноду
|
||||
</h1>
|
||||
<p className="text-slate-400">
|
||||
Простий спосіб підключити ваш комп'ютер до децентралізованої мережі AI
|
||||
<p className="text-gray-600">
|
||||
Інструкція з розгортання обчислювальної ноди DAARION
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Benefits */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-gradient-to-br from-purple-900/30 to-purple-950/30 border border-purple-800/30 rounded-xl p-6">
|
||||
<div className="bg-white border border-blue-100 rounded-xl p-6 shadow-sm">
|
||||
<div className="text-3xl mb-3">💰</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Заробляйте μGOV</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Отримуйте токени за надання обчислювальних ресурсів
|
||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Заробляйте токени</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Отримуйте винагороду за надання обчислювальних ресурсів
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-blue-900/30 to-blue-950/30 border border-blue-800/30 rounded-xl p-6">
|
||||
<div className="bg-white border border-purple-100 rounded-xl p-6 shadow-sm">
|
||||
<div className="text-3xl mb-3">🤖</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Доступ до AI</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Використовуйте AI моделі мережі безкоштовно
|
||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Доступ до AI</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Використовуйте AI моделі мережі безкоштовно для своїх агентів
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-green-900/30 to-green-950/30 border border-green-800/30 rounded-xl p-6">
|
||||
<div className="bg-white border border-green-100 rounded-xl p-6 shadow-sm">
|
||||
<div className="text-3xl mb-3">🌱</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Підтримайте спільноту</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Станьте частиною децентралізованої AI мережі
|
||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Розвивайте мережу</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Станьте частиною децентралізованої інфраструктури
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Requirements */}
|
||||
<div className="bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6 mb-8">
|
||||
<h2 className="text-xl font-bold mb-4">📋 Системні вимоги</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-6 h-6 text-purple-400" />
|
||||
<div>
|
||||
<div className="text-slate-400 text-sm">CPU</div>
|
||||
<div className="font-medium">4+ ядра</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Monitor className="w-6 h-6 text-blue-400" />
|
||||
<div>
|
||||
<div className="text-slate-400 text-sm">RAM</div>
|
||||
<div className="font-medium">8+ GB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<HardDrive className="w-6 h-6 text-green-400" />
|
||||
<div>
|
||||
<div className="text-slate-400 text-sm">Disk</div>
|
||||
<div className="font-medium">50+ GB вільно</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OS Selector */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedOS('macos')}
|
||||
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
selectedOS === 'macos'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
🍎 macOS
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedOS('linux')}
|
||||
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
selectedOS === 'linux'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
🐧 Linux
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedOS('windows')}
|
||||
className={`flex-1 py-3 px-4 rounded-lg font-medium transition-all ${
|
||||
selectedOS === 'windows'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
🪟 Windows
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold">{currentInstructions.title}</h2>
|
||||
|
||||
{currentInstructions.steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6"
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
|
||||
<p className="text-slate-400 text-sm mb-4">{step.description}</p>
|
||||
|
||||
{step.code && (
|
||||
<div className="relative">
|
||||
<pre className="bg-slate-950 border border-slate-700 rounded-lg p-4 overflow-x-auto text-sm">
|
||||
<code className="text-green-400">{step.code}</code>
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => copyToClipboard(step.code)}
|
||||
className="absolute top-2 right-2 p-2 bg-slate-800 hover:bg-slate-700 rounded-lg transition-colors"
|
||||
title="Скопіювати"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-slate-400" />
|
||||
)}
|
||||
</button>
|
||||
{/* Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Instructions */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 prose prose-blue max-w-none">
|
||||
{instructions ? (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({node, inline, className, children, ...props}: any) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline && match ? (
|
||||
<div className="relative group">
|
||||
<pre className={className} {...props}>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => copyToClipboard(String(children).replace(/\n$/, ''))}
|
||||
className="absolute top-2 right-2 p-2 bg-gray-700 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Copy"
|
||||
>
|
||||
{copied ? <CheckCircle className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{instructions}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Інструкції не знайдено. Зверніться до адміністратора.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step.link && (
|
||||
<a
|
||||
href={step.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Завантажити Python
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alternative: One-Click Installer */}
|
||||
<div className="mt-8 bg-gradient-to-r from-purple-900/30 to-pink-900/30 border border-purple-800/30 rounded-xl p-6">
|
||||
<h2 className="text-xl font-bold mb-4">⚡ Швидке підключення (Coming Soon)</h2>
|
||||
<p className="text-slate-400 mb-4">
|
||||
Скоро буде доступний інсталятор в один клік для автоматичного налаштування ноди
|
||||
</p>
|
||||
<button
|
||||
disabled
|
||||
className="px-6 py-3 bg-purple-600/50 text-white rounded-lg cursor-not-allowed opacity-50"
|
||||
>
|
||||
<Download className="w-4 h-4 inline mr-2" />
|
||||
Завантажити інсталятор (незабаром)
|
||||
</button>
|
||||
</div>
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Help Section */}
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-xl p-6">
|
||||
<h2 className="text-xl font-bold mb-4 text-blue-900 flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
Потрібна допомога?
|
||||
</h2>
|
||||
<div className="space-y-3 text-blue-800 text-sm">
|
||||
<p>Для отримання токенів доступу (NATS credentials) зверніться до адміністраторів:</p>
|
||||
<a
|
||||
href="https://matrix.to/#/#daarion:daarion.space"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-3 bg-white rounded-lg hover:shadow-md transition-shadow"
|
||||
>
|
||||
<span className="font-semibold">Matrix Chat</span>
|
||||
<ExternalLink className="w-4 h-4 ml-auto" />
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2 p-3 bg-white rounded-lg hover:shadow-md transition-shadow"
|
||||
>
|
||||
<span className="font-semibold">Discord Server</span>
|
||||
<ExternalLink className="w-4 h-4 ml-auto" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<div className="mt-8 bg-slate-900/50 backdrop-blur border border-slate-800 rounded-xl p-6">
|
||||
<h2 className="text-xl font-bold mb-4">❓ Потрібна допомога?</h2>
|
||||
<div className="space-y-2 text-slate-400">
|
||||
<p>• 📚 Документація: <a href="#" className="text-purple-400 hover:underline">docs.dagi.ai</a></p>
|
||||
<p>• 💬 Telegram спільнота: <a href="#" className="text-purple-400 hover:underline">@dagi_community</a></p>
|
||||
<p>• 🐛 Проблеми: <a href="#" className="text-purple-400 hover:underline">GitHub Issues</a></p>
|
||||
{/* System Requirements Summary */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h2 className="text-lg font-bold mb-4 text-gray-900">Вимоги</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">CPU</div>
|
||||
<div className="font-medium text-gray-900">4+ Cores</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Monitor className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">RAM</div>
|
||||
<div className="font-medium text-gray-900">16GB+ (32GB rec.)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<HardDrive className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">Storage</div>
|
||||
<div className="font-medium text-gray-900">100GB+ SSD</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Terminal className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">OS</div>
|
||||
<div className="font-medium text-gray-900">Ubuntu 22.04 / Debian</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle } from 'lucide-react';
|
||||
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle, Boxes, Shield } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiGet } from '../api/client';
|
||||
import { getNode2Agents, type Node2Agent } from '../api/node2Agents';
|
||||
import { getNode1Agents, type Node1Agent } from '../api/node1Agents';
|
||||
import { deployAgentToNode2, deployAllAgentsToNode2, checkNode2AgentsDeployment } from '../api/node2Deployment';
|
||||
@@ -41,6 +42,24 @@ interface NodeDetails {
|
||||
network_in: number;
|
||||
network_out: number;
|
||||
};
|
||||
microdaos?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
role?: string;
|
||||
}>;
|
||||
guardian_agent?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
status?: string;
|
||||
};
|
||||
steward_agent?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
status?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Grafana та Prometheus URLs (налаштувати під ваші сервери)
|
||||
@@ -103,6 +122,14 @@ export function NodeCabinetPage() {
|
||||
const agents = agentsData?.items || [];
|
||||
const isNode1 = nodeId?.includes('node-1');
|
||||
|
||||
// Отримуємо профіль з API (для MicroDAOs та агентів)
|
||||
let apiNodeProfile: any = null;
|
||||
try {
|
||||
apiNodeProfile = await apiGet(`/public/nodes/${nodeId}`);
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch node profile from API', e);
|
||||
}
|
||||
|
||||
// Отримуємо реальні дані з інвентаризації
|
||||
const inv = inventory;
|
||||
|
||||
@@ -195,6 +222,9 @@ export function NodeCabinetPage() {
|
||||
network_in: 1250,
|
||||
network_out: 890,
|
||||
},
|
||||
microdaos: apiNodeProfile?.microdaos || [],
|
||||
guardian_agent: apiNodeProfile?.guardian_agent,
|
||||
steward_agent: apiNodeProfile?.steward_agent,
|
||||
};
|
||||
},
|
||||
enabled: !!nodeId,
|
||||
@@ -551,6 +581,113 @@ export function NodeCabinetPage() {
|
||||
↓ {nodeDetails.metrics?.network_in || 0} MB/s ↑ {nodeDetails.metrics?.network_out || 0} MB/s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core Runtime & Participation */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Core Runtime */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-blue-600" />
|
||||
Core Runtime
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: 'Node Registry', icon: Database },
|
||||
{ name: 'NATS JetStream', icon: Network },
|
||||
{ name: 'Swapper Service', icon: RefreshCw },
|
||||
{ name: 'Ollama', icon: Bot },
|
||||
].map((service) => {
|
||||
const s = nodeDetails.services?.find(s => s.name.includes(service.name) || (service.name === 'Ollama' && s.name.includes('ollama')));
|
||||
const status = s?.status === 'running' ? 'online' : 'offline'; // Simple mapping
|
||||
const Icon = service.icon;
|
||||
|
||||
return (
|
||||
<div key={service.name} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-gray-700">{service.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${status === 'online' ? 'bg-green-500' : 'bg-gray-400'}`} />
|
||||
<span className="text-sm text-gray-600">{status === 'online' ? 'Active' : 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Guardian & Steward */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg border border-purple-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-4 h-4 text-purple-600" />
|
||||
<span className="font-medium text-purple-900">Guardian Agent</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-purple-700">
|
||||
{nodeDetails.guardian_agent?.name || 'Not active'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-4 h-4 text-blue-600" />
|
||||
<span className="font-medium text-blue-900">Steward Agent</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-blue-700">
|
||||
{nodeDetails.steward_agent?.name || 'Not active'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Participation */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<Boxes className="w-5 h-5 text-green-600" />
|
||||
Participation
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2 uppercase">Hosted MicroDAOs</h4>
|
||||
{nodeDetails.microdaos && nodeDetails.microdaos.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{nodeDetails.microdaos.map(dao => (
|
||||
<div key={dao.id} className="flex items-center justify-between p-3 bg-green-50 border border-green-100 rounded-lg">
|
||||
<span className="font-medium text-green-900">{dao.name}</span>
|
||||
<span className="text-xs px-2 py-1 bg-white text-green-700 rounded border border-green-200">
|
||||
Hosting
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
||||
<p className="text-gray-500 text-sm">No MicroDAOs hosted yet</p>
|
||||
<button
|
||||
onClick={() => navigate('/microdao')}
|
||||
className="mt-2 text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
Join a MicroDAO
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2 uppercase">Agent Capabilities</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="p-3 bg-gray-50 rounded-lg text-center">
|
||||
<div className="text-xl font-bold text-gray-900">{departments.length}</div>
|
||||
<div className="text-xs text-gray-500">Teams</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg text-center">
|
||||
<div className="text-xl font-bold text-gray-900">{nodeDetails.agents?.length || 0}</div>
|
||||
<div className="text-xs text-gray-500">Agents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Server, Activity, Cpu, HardDrive, Network, ArrowRight, RefreshCw, Zap } from 'lucide-react';
|
||||
import { Server, Activity, Cpu, HardDrive, Network, ArrowRight, RefreshCw, Zap, Plus } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
@@ -216,6 +216,13 @@ export function NodesPage() {
|
||||
<p className="text-gray-600 mt-2">Управління нодами та моніторинг системи</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/connect-node')}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Підключити ноду
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchAllNodesStatus}
|
||||
disabled={loading}
|
||||
|
||||
118
src/types/agent-cabinet.ts
Normal file
118
src/types/agent-cabinet.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
export interface HomeNodeView {
|
||||
id: string;
|
||||
name: string;
|
||||
hostname?: string;
|
||||
roles: string[];
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface MicrodaoBadge {
|
||||
id: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
role?: string;
|
||||
is_public: boolean;
|
||||
is_platform: boolean;
|
||||
}
|
||||
|
||||
export interface AgentSummary {
|
||||
id: string;
|
||||
slug?: string;
|
||||
display_name: string;
|
||||
title?: string;
|
||||
tagline?: string;
|
||||
kind: string;
|
||||
avatar_url?: string;
|
||||
status: string;
|
||||
node_id?: string;
|
||||
node_label?: string;
|
||||
home_node?: HomeNodeView;
|
||||
visibility_scope: string;
|
||||
is_listed_in_directory: boolean;
|
||||
is_system: boolean;
|
||||
is_public: boolean;
|
||||
is_orchestrator: boolean;
|
||||
primary_microdao_id?: string;
|
||||
primary_microdao_name?: string;
|
||||
primary_microdao_slug?: string;
|
||||
district?: string;
|
||||
microdaos: MicrodaoBadge[];
|
||||
public_skills: string[];
|
||||
}
|
||||
|
||||
export interface AgentVisibilityPayload {
|
||||
is_public: boolean;
|
||||
public_title?: string | null;
|
||||
public_tagline?: string | null;
|
||||
public_slug?: string | null;
|
||||
}
|
||||
|
||||
export interface AgentSystemPrompt {
|
||||
content: string;
|
||||
version: number;
|
||||
created_at: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface AgentSystemPrompts {
|
||||
core?: AgentSystemPrompt;
|
||||
safety?: AgentSystemPrompt;
|
||||
governance?: AgentSystemPrompt;
|
||||
tools?: AgentSystemPrompt;
|
||||
}
|
||||
|
||||
export interface AgentDetailDashboard {
|
||||
profile: {
|
||||
agent_id: string;
|
||||
display_name: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
node_id?: string;
|
||||
is_public: boolean;
|
||||
public_slug?: string;
|
||||
is_orchestrator: boolean;
|
||||
primary_microdao_id?: string;
|
||||
primary_microdao_name?: string;
|
||||
primary_microdao_slug?: string;
|
||||
avatar_url?: string;
|
||||
city_presence?: {
|
||||
primary_room_slug?: string;
|
||||
district?: string;
|
||||
}
|
||||
};
|
||||
node: {
|
||||
node_id: string;
|
||||
name: string;
|
||||
hostname?: string;
|
||||
roles: string[];
|
||||
environment?: string;
|
||||
status: string;
|
||||
guardian_agent?: { id: string; name: string; };
|
||||
steward_agent?: { id: string; name: string; };
|
||||
} | null;
|
||||
primary_city_room: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
matrix_room_id?: string;
|
||||
} | null;
|
||||
system_prompts: AgentSystemPrompts;
|
||||
public_profile: {
|
||||
is_public: boolean;
|
||||
public_slug?: string;
|
||||
public_title?: string;
|
||||
public_tagline?: string;
|
||||
public_skills: string[];
|
||||
public_district?: string;
|
||||
public_primary_room_slug?: string;
|
||||
} | null;
|
||||
microdao_memberships: {
|
||||
microdao_id: string;
|
||||
microdao_slug?: string;
|
||||
microdao_name?: string;
|
||||
logo_url?: string;
|
||||
role?: string;
|
||||
is_core: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user