feat: implement Task 029 (Agent Orchestrator & Visibility Flow)

This commit is contained in:
Apple
2025-11-28 10:17:57 -08:00
parent 1327295ff8
commit 69cc76fe00
3183 changed files with 1513720 additions and 129 deletions

View File

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

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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