feat: implement Task 029 (Agent Orchestrator & Visibility Flow)
This commit is contained in:
@@ -11,12 +11,13 @@ import {
|
||||
AgentMetricsCard,
|
||||
AgentSystemPromptsCard,
|
||||
AgentPublicProfileCard,
|
||||
AgentMicrodaoMembershipCard
|
||||
AgentMicrodaoMembershipCard,
|
||||
AgentVisibilityCard,
|
||||
CreateMicrodaoCard
|
||||
} from '@/components/agent-dashboard';
|
||||
import { AgentVisibilityCard } from '@/components/agent-dashboard/AgentVisibilityCard';
|
||||
import { api, Agent, AgentInvokeResponse } from '@/lib/api';
|
||||
import { updateAgentVisibility } from '@/lib/api/agents';
|
||||
import { AgentVisibilityPayload, VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
|
||||
import { VisibilityScope, getNodeBadgeLabel } from '@/lib/types/agents';
|
||||
import { updateAgentVisibility, AgentVisibilityUpdate } from '@/lib/api/agents';
|
||||
import { Bot, Settings, FileText, Building2, Cpu, MessageSquare, BarChart3, Users, Globe, Lock, Eye, EyeOff, ChevronLeft, Loader2 } from 'lucide-react';
|
||||
|
||||
// Tab types
|
||||
@@ -359,7 +360,7 @@ export default function AgentConsolePage() {
|
||||
|
||||
{/* Primary MicroDAO */}
|
||||
{profile?.primary_microdao_id && (
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4 mb-4">
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4">
|
||||
<div className="text-sm text-cyan-400 mb-1">Primary MicroDAO</div>
|
||||
<Link
|
||||
href={`/microdao/${profile.primary_microdao_slug}`}
|
||||
@@ -369,27 +370,16 @@ export default function AgentConsolePage() {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Orchestrator Actions */}
|
||||
{profile?.is_orchestrator && (
|
||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-amber-400 mb-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="font-medium">Orchestrator Privileges</span>
|
||||
</div>
|
||||
<p className="text-white/50 text-sm mb-3">
|
||||
As an orchestrator, this agent can create and manage MicroDAOs.
|
||||
</p>
|
||||
<button
|
||||
disabled
|
||||
className="px-4 py-2 bg-amber-500/20 text-amber-400 rounded-lg text-sm opacity-50 cursor-not-allowed"
|
||||
>
|
||||
Create MicroDAO (Coming Soon)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create MicroDAO / Orchestrator Actions */}
|
||||
<CreateMicrodaoCard
|
||||
agentId={dashboard.profile.agent_id}
|
||||
agentName={profile?.display_name || agentId}
|
||||
isOrchestrator={profile?.is_orchestrator ?? false}
|
||||
onCreated={refresh}
|
||||
/>
|
||||
|
||||
<AgentMicrodaoMembershipCard
|
||||
agentId={dashboard.profile.agent_id}
|
||||
memberships={dashboard.microdao_memberships ?? []}
|
||||
@@ -415,9 +405,10 @@ export default function AgentConsolePage() {
|
||||
{/* Visibility Settings */}
|
||||
<AgentVisibilityCard
|
||||
agentId={dashboard.profile.agent_id}
|
||||
visibilityScope={(dashboard.public_profile?.visibility_scope as VisibilityScope) || 'city'}
|
||||
isPublic={profile?.is_public ?? false}
|
||||
visibilityScope={(dashboard.public_profile?.visibility_scope as VisibilityScope) || 'global'}
|
||||
isListedInDirectory={dashboard.public_profile?.is_listed_in_directory ?? true}
|
||||
onUpdate={async (payload: AgentVisibilityPayload) => {
|
||||
onUpdate={async (payload: AgentVisibilityUpdate) => {
|
||||
await updateAgentVisibility(dashboard.profile.agent_id, payload);
|
||||
refresh();
|
||||
}}
|
||||
|
||||
44
apps/web/src/app/api/agents/[agentId]/microdao/route.ts
Normal file
44
apps/web/src/app/api/agents/[agentId]/microdao/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || 'http://daarion-city-service:7001';
|
||||
|
||||
/**
|
||||
* POST /api/agents/[agentId]/microdao
|
||||
* Create MicroDAO for agent (make agent an orchestrator)
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await context.params;
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${CITY_API_URL}/city/agents/${agentId}/microdao`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error('Failed to create microdao for agent:', response.status, text);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create MicroDAO', detail: text },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('Error creating microdao for agent:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || 'http://daarion-city-service:7001';
|
||||
|
||||
/**
|
||||
* PUT /api/microdao/[microdaoId]/visibility
|
||||
* Update MicroDAO visibility settings
|
||||
*/
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ microdaoId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { microdaoId } = await context.params;
|
||||
const body = await request.json();
|
||||
|
||||
const response = await fetch(`${CITY_API_URL}/city/microdao/${microdaoId}/visibility`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error('Failed to update microdao visibility:', response.status, text);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update MicroDAO visibility', detail: text },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('Error updating microdao visibility:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
|
||||
import { DISTRICT_COLORS } from "@/lib/microdao";
|
||||
import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard";
|
||||
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot } from "lucide-react";
|
||||
|
||||
export default function MicrodaoDetailPage() {
|
||||
@@ -370,6 +371,20 @@ export default function MicrodaoDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Visibility Settings (only for orchestrator) */}
|
||||
{orchestrator && (
|
||||
<MicrodaoVisibilityCard
|
||||
microdaoId={microdao.id}
|
||||
isPublic={microdao.is_public}
|
||||
isPlatform={microdao.is_platform}
|
||||
isOrchestrator={true} // TODO: check if current user is orchestrator
|
||||
onUpdated={() => {
|
||||
// Refresh the page data
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Eye, EyeOff, Users, Lock, Globe, Loader2 } from 'lucide-react';
|
||||
import { VisibilityScope, AgentVisibilityPayload } from '@/lib/types/agents';
|
||||
import { VisibilityScope } from '@/lib/types/agents';
|
||||
import { AgentVisibilityUpdate } from '@/lib/api/agents';
|
||||
|
||||
interface AgentVisibilityCardProps {
|
||||
agentId: string;
|
||||
isPublic: boolean;
|
||||
visibilityScope: VisibilityScope;
|
||||
isListedInDirectory: boolean;
|
||||
onUpdate?: (payload: AgentVisibilityPayload) => Promise<void>;
|
||||
onUpdate?: (payload: AgentVisibilityUpdate) => Promise<void>;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
@@ -35,36 +37,68 @@ const VISIBILITY_OPTIONS: { value: VisibilityScope; label: string; description:
|
||||
|
||||
export function AgentVisibilityCard({
|
||||
agentId,
|
||||
isPublic,
|
||||
visibilityScope,
|
||||
isListedInDirectory,
|
||||
onUpdate,
|
||||
readOnly = false,
|
||||
}: AgentVisibilityCardProps) {
|
||||
const [publicState, setPublicState] = useState(isPublic);
|
||||
const [scope, setScope] = useState<VisibilityScope>(visibilityScope);
|
||||
const [listed, setListed] = useState(isListedInDirectory);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handlePublicToggle = async (checked: boolean) => {
|
||||
if (readOnly || saving) return;
|
||||
|
||||
setPublicState(checked);
|
||||
setError(null);
|
||||
|
||||
// If making private, also update scope
|
||||
const newScope = checked ? scope : 'private';
|
||||
if (!checked) {
|
||||
setScope('private');
|
||||
setListed(false);
|
||||
}
|
||||
|
||||
if (onUpdate) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onUpdate({ is_public: checked, visibility_scope: newScope });
|
||||
} catch (e) {
|
||||
setError('Не вдалося зберегти');
|
||||
setPublicState(isPublic);
|
||||
setScope(visibilityScope);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleScopeChange = async (newScope: VisibilityScope) => {
|
||||
if (readOnly || saving) return;
|
||||
|
||||
setScope(newScope);
|
||||
setError(null);
|
||||
|
||||
// If changing to non-global, auto-unlist from directory
|
||||
const newListed = newScope === 'global' ? listed : false;
|
||||
if (newScope !== 'global') {
|
||||
// If changing to global, make public
|
||||
const newPublic = newScope === 'global' ? true : publicState;
|
||||
if (newScope === 'global') {
|
||||
setPublicState(true);
|
||||
} else if (newScope === 'private') {
|
||||
setPublicState(false);
|
||||
setListed(false);
|
||||
}
|
||||
|
||||
if (onUpdate) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onUpdate({ visibility_scope: newScope, is_listed_in_directory: newListed });
|
||||
await onUpdate({ is_public: newPublic, visibility_scope: newScope });
|
||||
} catch (e) {
|
||||
setError('Не вдалося зберегти');
|
||||
setScope(visibilityScope);
|
||||
setListed(isListedInDirectory);
|
||||
setPublicState(isPublic);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -77,10 +111,11 @@ export function AgentVisibilityCard({
|
||||
setListed(checked);
|
||||
setError(null);
|
||||
|
||||
// is_listed_in_directory is tied to is_public in the backend
|
||||
if (onUpdate) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onUpdate({ visibility_scope: scope, is_listed_in_directory: checked });
|
||||
await onUpdate({ is_public: checked, visibility_scope: scope });
|
||||
} catch (e) {
|
||||
setError('Не вдалося зберегти');
|
||||
setListed(isListedInDirectory);
|
||||
@@ -106,6 +141,40 @@ export function AgentVisibilityCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Citizen Toggle */}
|
||||
<div className="mb-4 p-4 bg-white/5 border border-white/10 rounded-lg">
|
||||
<label className="flex items-center justify-between cursor-pointer">
|
||||
<div>
|
||||
<div className="text-white font-medium flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-cyan-400" />
|
||||
Публічний громадянин міста
|
||||
</div>
|
||||
<div className="text-xs text-white/50 mt-1">
|
||||
{publicState
|
||||
? 'Агент видимий у /citizens та публічних сервісах'
|
||||
: 'Агент прихований від публічного доступу'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={publicState}
|
||||
onChange={(e) => handlePublicToggle(e.target.checked)}
|
||||
disabled={readOnly || saving}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-11 h-6 rounded-full transition-colors ${
|
||||
publicState ? 'bg-cyan-500' : 'bg-white/20'
|
||||
} peer-focus:ring-2 peer-focus:ring-cyan-500/50`}>
|
||||
<div className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
||||
publicState ? 'translate-x-5' : 'translate-x-0'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-white/50 mb-3">Режим видимості:</div>
|
||||
<div className="space-y-3">
|
||||
{VISIBILITY_OPTIONS.map((option) => (
|
||||
<button
|
||||
|
||||
343
apps/web/src/components/agent-dashboard/CreateMicrodaoCard.tsx
Normal file
343
apps/web/src/components/agent-dashboard/CreateMicrodaoCard.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Building2, Plus, Globe, Lock, Layers, Loader2, CheckCircle } from 'lucide-react';
|
||||
import { createMicrodaoForAgent, MicrodaoCreateRequest } from '@/lib/api/agents';
|
||||
|
||||
interface CreateMicrodaoCardProps {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
isOrchestrator: boolean;
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
export function CreateMicrodaoCard({
|
||||
agentId,
|
||||
agentName,
|
||||
isOrchestrator,
|
||||
onCreated,
|
||||
}: CreateMicrodaoCardProps) {
|
||||
const router = useRouter();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
const [slug, setSlug] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [makePlatform, setMakePlatform] = useState(false);
|
||||
const [isPublic, setIsPublic] = useState(true);
|
||||
|
||||
const generateSlug = (value: string) => {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
};
|
||||
|
||||
const handleNameChange = (value: string) => {
|
||||
setName(value);
|
||||
// Auto-generate slug if not manually edited
|
||||
if (!slug || slug === generateSlug(name)) {
|
||||
setSlug(generateSlug(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !slug.trim() || saving) return;
|
||||
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
const payload: MicrodaoCreateRequest = {
|
||||
name: name.trim(),
|
||||
slug: slug.trim(),
|
||||
description: description.trim() || undefined,
|
||||
make_platform: makePlatform,
|
||||
is_public: isPublic,
|
||||
};
|
||||
|
||||
const result = await createMicrodaoForAgent(agentId, payload);
|
||||
|
||||
setSuccess(`MicroDAO "${result.microdao.name}" успішно створено!`);
|
||||
|
||||
// Reset form
|
||||
setName('');
|
||||
setSlug('');
|
||||
setDescription('');
|
||||
setMakePlatform(false);
|
||||
setIsPublic(true);
|
||||
setExpanded(false);
|
||||
|
||||
onCreated?.();
|
||||
|
||||
// Navigate to new MicroDAO after a short delay
|
||||
setTimeout(() => {
|
||||
router.push(`/microdao/${result.microdao.slug}`);
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Не вдалося створити MicroDAO');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// If already an orchestrator, show different UI
|
||||
if (isOrchestrator) {
|
||||
return (
|
||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 text-amber-400 mb-3">
|
||||
<Building2 className="w-5 h-5" />
|
||||
<h3 className="text-lg font-semibold">Orchestrator</h3>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
Цей агент є оркестратором і може керувати MicroDAO.
|
||||
</p>
|
||||
|
||||
{!expanded ? (
|
||||
<button
|
||||
onClick={() => setExpanded(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500/20 hover:bg-amber-500/30 text-amber-400 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Створити ще один MicroDAO
|
||||
</button>
|
||||
) : (
|
||||
<MicrodaoForm
|
||||
name={name}
|
||||
slug={slug}
|
||||
description={description}
|
||||
makePlatform={makePlatform}
|
||||
isPublic={isPublic}
|
||||
saving={saving}
|
||||
error={error}
|
||||
success={success}
|
||||
onNameChange={handleNameChange}
|
||||
onSlugChange={setSlug}
|
||||
onDescriptionChange={setDescription}
|
||||
onMakePlatformChange={setMakePlatform}
|
||||
onIsPublicChange={setIsPublic}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => setExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Not an orchestrator - show option to become one
|
||||
return (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 text-white mb-3">
|
||||
<Building2 className="w-5 h-5 text-cyan-400" />
|
||||
<h3 className="text-lg font-semibold">Створити MicroDAO</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
Зробіть цього агента оркестратором і створіть власний MicroDAO.
|
||||
Оркестратор може керувати членами DAO, налаштовувати канали та інтеграції.
|
||||
</p>
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-3 bg-emerald-500/20 border border-emerald-500/30 rounded-lg text-emerald-300 text-sm flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!expanded ? (
|
||||
<button
|
||||
onClick={() => setExpanded(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cyan-500/20 hover:bg-cyan-500/30 text-cyan-400 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Стати оркестратором і створити MicroDAO
|
||||
</button>
|
||||
) : (
|
||||
<MicrodaoForm
|
||||
name={name}
|
||||
slug={slug}
|
||||
description={description}
|
||||
makePlatform={makePlatform}
|
||||
isPublic={isPublic}
|
||||
saving={saving}
|
||||
error={error}
|
||||
success={success}
|
||||
onNameChange={handleNameChange}
|
||||
onSlugChange={setSlug}
|
||||
onDescriptionChange={setDescription}
|
||||
onMakePlatformChange={setMakePlatform}
|
||||
onIsPublicChange={setIsPublic}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => setExpanded(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Extracted form component
|
||||
interface MicrodaoFormProps {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
makePlatform: boolean;
|
||||
isPublic: boolean;
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
success: string | null;
|
||||
onNameChange: (value: string) => void;
|
||||
onSlugChange: (value: string) => void;
|
||||
onDescriptionChange: (value: string) => void;
|
||||
onMakePlatformChange: (value: boolean) => void;
|
||||
onIsPublicChange: (value: boolean) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function MicrodaoForm({
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
makePlatform,
|
||||
isPublic,
|
||||
saving,
|
||||
error,
|
||||
success,
|
||||
onNameChange,
|
||||
onSlugChange,
|
||||
onDescriptionChange,
|
||||
onMakePlatformChange,
|
||||
onIsPublicChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: MicrodaoFormProps) {
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-4 mt-4 pt-4 border-t border-white/10">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/30 rounded-lg text-red-300 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="p-3 bg-emerald-500/20 border border-emerald-500/30 rounded-lg text-emerald-300 text-sm flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">Назва MicroDAO *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="My Awesome DAO"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50"
|
||||
required
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">Slug (URL) *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={slug}
|
||||
onChange={(e) => onSlugChange(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
||||
placeholder="my-awesome-dao"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50 font-mono text-sm"
|
||||
required
|
||||
disabled={saving}
|
||||
/>
|
||||
<p className="text-xs text-white/40 mt-1">URL: /microdao/{slug || 'your-slug'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">Опис</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
placeholder="Короткий опис вашого MicroDAO..."
|
||||
rows={3}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder-white/30 focus:outline-none focus:border-cyan-500/50 resize-none"
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isPublic}
|
||||
onChange={(e) => onIsPublicChange(e.target.checked)}
|
||||
disabled={saving}
|
||||
className="w-4 h-4 rounded border-white/30 bg-white/10 text-cyan-500 focus:ring-cyan-500/50"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{isPublic ? <Globe className="w-4 h-4 text-cyan-400" /> : <Lock className="w-4 h-4 text-white/50" />}
|
||||
<span className="text-white">Публічний MicroDAO</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={makePlatform}
|
||||
onChange={(e) => onMakePlatformChange(e.target.checked)}
|
||||
disabled={saving}
|
||||
className="w-4 h-4 rounded border-white/30 bg-white/10 text-amber-500 focus:ring-amber-500/50"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-white">Це платформа / District</span>
|
||||
</div>
|
||||
</label>
|
||||
<p className="text-xs text-white/40 ml-7">
|
||||
Платформа може мати дочірні MicroDAO
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim() || !slug.trim() || saving}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-cyan-500 hover:bg-cyan-400 disabled:bg-white/10 disabled:text-white/30 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Створення...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
Створити MicroDAO
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-white/5 hover:bg-white/10 text-white/70 rounded-lg transition-colors"
|
||||
>
|
||||
Скасувати
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export { AgentMetricsCard } from './AgentMetricsCard';
|
||||
export { AgentSystemPromptsCard } from './AgentSystemPromptsCard';
|
||||
export { AgentPublicProfileCard } from './AgentPublicProfileCard';
|
||||
export { AgentMicrodaoMembershipCard } from './AgentMicrodaoMembershipCard';
|
||||
|
||||
export { AgentVisibilityCard } from './AgentVisibilityCard';
|
||||
export { CreateMicrodaoCard } from './CreateMicrodaoCard';
|
||||
|
||||
159
apps/web/src/components/microdao/MicrodaoVisibilityCard.tsx
Normal file
159
apps/web/src/components/microdao/MicrodaoVisibilityCard.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Eye, Globe, Lock, Layers, Loader2, Settings } from 'lucide-react';
|
||||
import { updateMicrodaoVisibility } from '@/lib/api/microdao';
|
||||
|
||||
interface MicrodaoVisibilityCardProps {
|
||||
microdaoId: string;
|
||||
isPublic: boolean;
|
||||
isPlatform: boolean;
|
||||
isOrchestrator: boolean; // Only orchestrator can edit
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export function MicrodaoVisibilityCard({
|
||||
microdaoId,
|
||||
isPublic,
|
||||
isPlatform,
|
||||
isOrchestrator,
|
||||
onUpdated,
|
||||
}: MicrodaoVisibilityCardProps) {
|
||||
const [publicState, setPublicState] = useState(isPublic);
|
||||
const [platformState, setPlatformState] = useState(isPlatform);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handlePublicToggle = async (checked: boolean) => {
|
||||
if (!isOrchestrator || saving) return;
|
||||
|
||||
setPublicState(checked);
|
||||
setError(null);
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
await updateMicrodaoVisibility(microdaoId, {
|
||||
is_public: checked,
|
||||
is_platform: platformState,
|
||||
});
|
||||
onUpdated?.();
|
||||
} catch (e) {
|
||||
setError('Не вдалося зберегти');
|
||||
setPublicState(isPublic);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlatformToggle = async (checked: boolean) => {
|
||||
if (!isOrchestrator || saving) return;
|
||||
|
||||
setPlatformState(checked);
|
||||
setError(null);
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
await updateMicrodaoVisibility(microdaoId, {
|
||||
is_public: publicState,
|
||||
is_platform: checked,
|
||||
});
|
||||
onUpdated?.();
|
||||
} catch (e) {
|
||||
setError('Не вдалося зберегти');
|
||||
setPlatformState(isPlatform);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOrchestrator) {
|
||||
return null; // Don't show settings to non-orchestrators
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-cyan-400" />
|
||||
Налаштування видимості
|
||||
</h2>
|
||||
{saving && <Loader2 className="w-4 h-4 text-cyan-400 animate-spin" />}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/30 rounded-lg text-red-300 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Public toggle */}
|
||||
<div className="bg-slate-900/50 border border-slate-700/30 rounded-lg p-4">
|
||||
<label className="flex items-center justify-between cursor-pointer">
|
||||
<div>
|
||||
<div className="text-slate-200 font-medium flex items-center gap-2">
|
||||
{publicState ? <Globe className="w-4 h-4 text-cyan-400" /> : <Lock className="w-4 h-4 text-slate-500" />}
|
||||
Публічний MicroDAO
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{publicState
|
||||
? 'Видимий у списку MicroDAO та на City Map'
|
||||
: 'Прихований від публічного доступу'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={publicState}
|
||||
onChange={(e) => handlePublicToggle(e.target.checked)}
|
||||
disabled={saving}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-11 h-6 rounded-full transition-colors ${
|
||||
publicState ? 'bg-cyan-500' : 'bg-slate-600'
|
||||
} peer-focus:ring-2 peer-focus:ring-cyan-500/50`}>
|
||||
<div className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
||||
publicState ? 'translate-x-5' : 'translate-x-0'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Platform toggle */}
|
||||
<div className="bg-slate-900/50 border border-slate-700/30 rounded-lg p-4">
|
||||
<label className="flex items-center justify-between cursor-pointer">
|
||||
<div>
|
||||
<div className="text-slate-200 font-medium flex items-center gap-2">
|
||||
<Layers className={`w-4 h-4 ${platformState ? 'text-amber-400' : 'text-slate-500'}`} />
|
||||
Платформа / District
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{platformState
|
||||
? 'Може мати дочірні MicroDAO, виділяється на City Map'
|
||||
: 'Звичайний MicroDAO без дочірніх структур'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={platformState}
|
||||
onChange={(e) => handlePlatformToggle(e.target.checked)}
|
||||
disabled={saving}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-11 h-6 rounded-full transition-colors ${
|
||||
platformState ? 'bg-amber-500' : 'bg-slate-600'
|
||||
} peer-focus:ring-2 peer-focus:ring-amber-500/50`}>
|
||||
<div className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
|
||||
platformState ? 'translate-x-5' : 'translate-x-0'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,87 @@
|
||||
import { AgentVisibilityPayload } from '@/lib/types/agents';
|
||||
/**
|
||||
* Agent API Client (Task 029)
|
||||
*/
|
||||
|
||||
import type { AgentSummary } from "@/lib/types/agents";
|
||||
import type { MicrodaoDetail } from "@/lib/types/microdao";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export type VisibilityScope = "global" | "microdao" | "private";
|
||||
|
||||
export interface AgentVisibilityUpdate {
|
||||
is_public: boolean;
|
||||
visibility_scope?: VisibilityScope;
|
||||
}
|
||||
|
||||
export interface MicrodaoCreateRequest {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
make_platform?: boolean;
|
||||
is_public?: boolean;
|
||||
parent_microdao_id?: string | null;
|
||||
}
|
||||
|
||||
export interface MicrodaoCreateResponse {
|
||||
status: string;
|
||||
microdao: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
is_public: boolean;
|
||||
is_platform: boolean;
|
||||
};
|
||||
agent_id: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Update agent visibility settings
|
||||
*/
|
||||
export async function updateAgentVisibility(
|
||||
agentId: string,
|
||||
payload: AgentVisibilityPayload
|
||||
): Promise<void> {
|
||||
const response = await fetch(`/api/agents/${agentId}/visibility`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: AgentVisibilityUpdate
|
||||
): Promise<{ status: string; agent_id: string; is_public: boolean; visibility_scope: string }> {
|
||||
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/visibility`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(json?.detail || json?.error || "Failed to update visibility");
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MicroDAO for agent (make agent an orchestrator)
|
||||
*/
|
||||
export async function createMicrodaoForAgent(
|
||||
agentId: string,
|
||||
payload: MicrodaoCreateRequest
|
||||
): Promise<MicrodaoCreateResponse> {
|
||||
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/microdao`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to update visibility');
|
||||
}
|
||||
}
|
||||
const json = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(json?.detail || json?.error || "Failed to create MicroDAO");
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -1,69 +1,46 @@
|
||||
import type {
|
||||
AgentMicrodaoMembership,
|
||||
MicrodaoOption,
|
||||
} from "@/lib/microdao";
|
||||
/**
|
||||
* MicroDAO API Client (Task 029)
|
||||
*/
|
||||
|
||||
async function request<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(input, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface MicrodaoVisibilityUpdate {
|
||||
is_public: boolean;
|
||||
is_platform?: boolean;
|
||||
}
|
||||
|
||||
export interface MicrodaoVisibilityResponse {
|
||||
status: string;
|
||||
microdao_id: string;
|
||||
slug: string;
|
||||
is_public: boolean;
|
||||
is_platform: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Update MicroDAO visibility settings
|
||||
*/
|
||||
export async function updateMicrodaoVisibility(
|
||||
microdaoId: string,
|
||||
data: MicrodaoVisibilityUpdate
|
||||
): Promise<MicrodaoVisibilityResponse> {
|
||||
const res = await fetch(`/api/microdao/${encodeURIComponent(microdaoId)}/visibility`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
const json = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
const message =
|
||||
(data && (data.error || data.detail || data.message)) ||
|
||||
"Request failed";
|
||||
throw new Error(message);
|
||||
throw new Error(json?.detail || json?.error || "Failed to update MicroDAO visibility");
|
||||
}
|
||||
|
||||
return data as T;
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function fetchMicrodaoOptions(): Promise<MicrodaoOption[]> {
|
||||
const data = await request<{ items?: MicrodaoOption[] }>(
|
||||
"/api/microdao/options"
|
||||
);
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
export async function assignAgentToMicrodao(
|
||||
agentId: string,
|
||||
payload: { microdao_id: string; role?: string; is_core?: boolean }
|
||||
): Promise<AgentMicrodaoMembership> {
|
||||
return request<AgentMicrodaoMembership>(
|
||||
`/api/agents/${encodeURIComponent(agentId)}/microdao-membership`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeAgentFromMicrodao(
|
||||
agentId: string,
|
||||
microdaoId: string
|
||||
): Promise<void> {
|
||||
const res = await fetch(
|
||||
`/api/agents/${encodeURIComponent(
|
||||
agentId
|
||||
)}/microdao-membership/${encodeURIComponent(microdaoId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
const message =
|
||||
(data && (data.error || data.detail || data.message)) ||
|
||||
"Failed to remove MicroDAO membership";
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user