suna/frontend/src/app/(dashboard)/marketplace/my-templates/page.tsx

343 lines
14 KiB
TypeScript
Raw Normal View History

2025-06-07 14:24:59 +08:00
'use client';
import React, { useState } from 'react';
2025-07-07 00:17:29 +08:00
import { Globe, GlobeLock, Download, Calendar, User, Tags, Loader2, AlertTriangle, Plus, GitBranch, Edit2, Eye } from 'lucide-react';
2025-06-07 14:24:59 +08:00
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
2025-07-07 00:17:29 +08:00
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { useMyTemplates, useUnpublishTemplate, usePublishTemplate } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
2025-06-07 14:24:59 +08:00
import { toast } from 'sonner';
import { getAgentAvatar } from '../../agents/_utils/get-agent-style';
import { Skeleton } from '@/components/ui/skeleton';
import Link from 'next/link';
2025-07-07 00:17:29 +08:00
interface PublishDialogData {
templateId: string;
templateName: string;
currentTags: string[];
}
2025-06-07 14:24:59 +08:00
export default function MyTemplatesPage() {
2025-07-07 00:17:29 +08:00
const [actioningId, setActioningId] = useState<string | null>(null);
const [publishDialog, setPublishDialog] = useState<PublishDialogData | null>(null);
const [publishTags, setPublishTags] = useState('');
2025-06-07 14:24:59 +08:00
const { data: templates, isLoading, error } = useMyTemplates();
const unpublishMutation = useUnpublishTemplate();
2025-07-07 00:17:29 +08:00
const publishMutation = usePublishTemplate();
2025-06-07 14:24:59 +08:00
const handleUnpublish = async (templateId: string, templateName: string) => {
try {
2025-07-07 00:17:29 +08:00
setActioningId(templateId);
2025-06-07 14:24:59 +08:00
await unpublishMutation.mutateAsync(templateId);
toast.success(`${templateName} has been unpublished from the marketplace`);
} catch (error: any) {
toast.error(error.message || 'Failed to unpublish template');
} finally {
2025-07-07 00:17:29 +08:00
setActioningId(null);
}
};
const openPublishDialog = (template: any) => {
setPublishDialog({
templateId: template.template_id,
templateName: template.name,
currentTags: template.tags || []
});
setPublishTags((template.tags || []).join(', '));
};
const handlePublish = async () => {
if (!publishDialog) return;
try {
setActioningId(publishDialog.templateId);
// Parse tags from comma-separated string
const tags = publishTags
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
await publishMutation.mutateAsync({
template_id: publishDialog.templateId,
tags: tags.length > 0 ? tags : undefined
});
toast.success(`${publishDialog.templateName} has been published to the marketplace`);
setPublishDialog(null);
setPublishTags('');
} catch (error: any) {
toast.error(error.message || 'Failed to publish template');
} finally {
setActioningId(null);
2025-06-07 14:24:59 +08:00
}
};
const getTemplateStyling = (template: any) => {
if (template.avatar && template.avatar_color) {
return {
avatar: template.avatar,
color: template.avatar_color,
};
}
return getAgentAvatar(template.template_id);
};
if (error) {
return (
<div className="container mx-auto max-w-6xl px-4 py-8">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Failed to load your templates. Please try again later.
</AlertDescription>
</Alert>
</div>
);
}
return (
2025-07-07 00:17:29 +08:00
<>
<div className="container mx-auto max-w-6xl px-4 py-8">
<div className="space-y-8">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold tracking-tight">
My Templates
</h1>
<p className="text-muted-foreground">
Manage your secure agent templates and marketplace presence
</p>
</div>
2025-06-07 14:24:59 +08:00
</div>
</div>
2025-07-07 00:17:29 +08:00
{isLoading ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden">
<Skeleton className="h-32 w-full" />
<div className="p-4 space-y-3">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-2/3" />
<Skeleton className="h-8 w-full" />
</div>
2025-06-07 14:24:59 +08:00
</div>
2025-07-07 00:17:29 +08:00
))}
</div>
) : templates?.length === 0 ? (
<div className="text-center py-12">
<div className="mx-auto w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<Globe className="h-8 w-8 text-muted-foreground" />
2025-06-07 14:24:59 +08:00
</div>
2025-07-07 00:17:29 +08:00
<h3 className="text-lg font-medium mb-2">No templates yet</h3>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
Create your first secure agent template to share with the community while keeping your credentials safe.
</p>
<Link href="/agents">
<Button>
<Plus className="h-4 w-4 mr-2" />
Create Your First Agent
</Button>
</Link>
2025-06-07 14:24:59 +08:00
</div>
2025-07-07 00:17:29 +08:00
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{templates?.map((template) => {
const { avatar, color } = getTemplateStyling(template);
const isActioning = actioningId === template.template_id;
return (
<div
key={template.template_id}
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 flex flex-col h-full"
>
<div className={`h-32 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
<div className="text-4xl">
{avatar}
</div>
<div className="absolute top-2 right-2">
{template.is_public ? (
<Badge variant="default" className="bg-green-100 text-green-800 border-green-200">
<Globe className="h-3 w-3 mr-1" />
Public
2025-06-07 14:24:59 +08:00
</Badge>
2025-07-07 00:17:29 +08:00
) : (
<Badge variant="secondary" className="bg-gray-100 text-gray-700 border-gray-200">
<GlobeLock className="h-3 w-3 mr-1" />
Private
2025-06-07 14:24:59 +08:00
</Badge>
)}
</div>
</div>
2025-07-07 00:17:29 +08:00
<div className="p-4 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{template.name}
</h3>
{template.metadata?.source_version_name && (
<Badge variant="secondary" className="text-xs shrink-0">
<GitBranch className="h-3 w-3" />
{template.metadata.source_version_name}
</Badge>
)}
</div>
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
{template.description || 'No description available'}
</p>
{template.tags && template.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{template.tags.slice(0, 2).map(tag => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
{template.tags.length > 2 && (
<Badge variant="outline" className="text-xs">
+{template.tags.length - 2}
</Badge>
2025-06-07 14:24:59 +08:00
)}
</div>
)}
2025-07-07 00:17:29 +08:00
<div className="space-y-1 mb-4">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
<span>Created {new Date(template.created_at).toLocaleDateString()}</span>
</div>
{template.is_public && template.marketplace_published_at && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Globe className="h-3 w-3" />
<span>Published {new Date(template.marketplace_published_at).toLocaleDateString()}</span>
</div>
)}
{template.is_public && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Download className="h-3 w-3" />
<span>{template.download_count} downloads</span>
</div>
)}
</div>
<div className="mt-auto space-y-2">
{template.is_public ? (
<>
<Button
onClick={() => handleUnpublish(template.template_id, template.name)}
disabled={isActioning}
variant="outline"
className="w-full"
size="sm"
>
{isActioning ? (
<>
<Loader2 className="h-3 w-3 animate-spin mr-2" />
Unpublishing...
</>
) : (
<>
<GlobeLock className="h-3 w-3 mr-2" />
Make Private
</>
)}
</Button>
<Link href={`/marketplace?search=${encodeURIComponent(template.name)}`} className="w-full">
<Button variant="ghost" size="sm" className="w-full">
<Eye className="h-3 w-3 mr-2" />
View in Marketplace
</Button>
</Link>
</>
) : (
<Button
onClick={() => openPublishDialog(template)}
disabled={isActioning}
variant="default"
className="w-full"
size="sm"
>
{isActioning ? (
<>
<Loader2 className="h-3 w-3 animate-spin mr-2" />
Publishing...
</>
) : (
<>
<Globe className="h-3 w-3 mr-2" />
Publish to Marketplace
</>
)}
</Button>
)}
</div>
2025-06-07 14:24:59 +08:00
</div>
</div>
2025-07-07 00:17:29 +08:00
);
})}
</div>
)}
</div>
2025-06-07 14:24:59 +08:00
</div>
2025-07-07 00:17:29 +08:00
{/* Publish Dialog */}
<Dialog open={!!publishDialog} onOpenChange={() => setPublishDialog(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Publish Template to Marketplace</DialogTitle>
<DialogDescription>
Make "{publishDialog?.templateName}" available for the community to discover and install.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="tags">Tags (optional)</Label>
<Input
id="tags"
placeholder="automation, productivity, data-analysis"
value={publishTags}
onChange={(e) => setPublishTags(e.target.value)}
className="mt-1"
/>
<p className="text-xs text-muted-foreground mt-1">
Separate tags with commas to help users discover your template
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setPublishDialog(null)}
disabled={!!actioningId}
>
Cancel
</Button>
<Button
onClick={handlePublish}
disabled={!!actioningId}
>
{actioningId ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Publishing...
</>
) : (
<>
<Globe className="h-4 w-4 mr-2" />
Publish Template
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
2025-06-07 14:24:59 +08:00
);
}