Files
patchy631--ai-engineering-hub/open-agent-builder/components/app/(home)/sections/workflow-builder/SaveAsTemplateModal.tsx
T
2026-07-13 12:37:47 +08:00

364 lines
14 KiB
TypeScript

"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, Save, Tag, FolderOpen, Clock, BarChart3, Globe, Lock, Info } from "lucide-react";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { toast } from "sonner";
import { useUser } from "@clerk/nextjs";
interface SaveAsTemplateModalProps {
isOpen: boolean;
onClose: () => void;
workflowId: string;
workflowName: string;
}
const categories = [
"User Templates",
"AI & Automation",
"Data Processing",
"Web Scraping",
"Business Logic",
"Integration",
"Utility",
"Custom",
];
const difficultyLevels = [
{ value: "beginner", label: "Beginner", color: "text-green-600" },
{ value: "intermediate", label: "Intermediate", color: "text-yellow-600" },
{ value: "advanced", label: "Advanced", color: "text-orange-600" },
{ value: "expert", label: "Expert", color: "text-red-600" },
];
export default function SaveAsTemplateModal({
isOpen,
onClose,
workflowId,
workflowName,
}: SaveAsTemplateModalProps) {
const { user } = useUser();
const saveAsTemplate = useMutation(api.templates.saveAsTemplate);
const [formData, setFormData] = useState({
name: `${workflowName} Template`,
description: "",
category: "User Templates",
tags: [] as string[],
difficulty: "intermediate",
estimatedTime: "",
isPublic: false,
});
const [currentTag, setCurrentTag] = useState("");
const [isSaving, setIsSaving] = useState(false);
const handleAddTag = () => {
if (currentTag.trim() && !formData.tags.includes(currentTag.trim())) {
setFormData({
...formData,
tags: [...formData.tags, currentTag.trim()],
});
setCurrentTag("");
}
};
const handleRemoveTag = (tag: string) => {
setFormData({
...formData,
tags: formData.tags.filter((t) => t !== tag),
});
};
const handleSave = async () => {
if (!formData.name.trim()) {
toast.error("Template name is required");
return;
}
if (!workflowId) {
toast.error("Invalid workflow");
return;
}
setIsSaving(true);
try {
await saveAsTemplate({
workflowId: workflowId as any,
name: formData.name,
description: formData.description || undefined,
category: formData.category,
tags: formData.tags.length > 0 ? formData.tags : undefined,
difficulty: formData.difficulty || undefined,
estimatedTime: formData.estimatedTime || undefined,
isPublic: formData.isPublic,
});
toast.success("Template saved successfully!", {
description: formData.isPublic
? "Your template is now available to other users"
: "Your template has been saved to your private collection",
});
onClose();
} catch (error) {
console.error("Failed to save template:", error);
toast.error("Failed to save template", {
description: error instanceof Error ? error.message : "Please try again",
});
} finally {
setIsSaving(false);
}
};
if (!isOpen) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 z-[100] flex items-center justify-center p-20"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ type: "spring", duration: 0.3 }}
className="bg-accent-white rounded-16 shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-24 border-b border-border-faint">
<div className="flex items-center justify-between">
<div className="flex items-center gap-12">
<div className="w-40 h-40 rounded-10 bg-heat-100 flex items-center justify-center">
<Save className="w-20 h-20 text-white" />
</div>
<div>
<h2 className="text-title-h4 text-accent-black">Save as Template</h2>
<p className="text-body-small text-black-alpha-48 mt-2">
Save this workflow as a reusable template
</p>
</div>
</div>
<button
onClick={onClose}
className="w-32 h-32 rounded-8 hover:bg-black-alpha-4 transition-colors flex items-center justify-center"
>
<X className="w-16 h-16" />
</button>
</div>
</div>
{/* Content */}
<div className="p-24 overflow-y-auto flex-1 space-y-20">
{/* Template Name */}
<div>
<label className="text-body-small font-medium text-accent-black mb-8 block">
Template Name *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter template name"
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
/>
</div>
{/* Description */}
<div>
<label className="text-body-small font-medium text-accent-black mb-8 block">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe what this template does and when to use it"
rows={3}
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100 resize-none"
/>
</div>
{/* Category */}
<div>
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
<FolderOpen className="w-14 h-14" />
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:ring-2 focus:ring-heat-100"
>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>
{/* Tags */}
<div>
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
<Tag className="w-14 h-14" />
Tags
</label>
<div className="flex gap-8 mb-8">
<input
type="text"
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddTag();
}
}}
placeholder="Add tags (press Enter)"
className="flex-1 px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
/>
<button
onClick={handleAddTag}
className="px-16 py-10 bg-black-alpha-4 hover:bg-black-alpha-8 text-accent-black rounded-8 text-body-small font-medium transition-all"
>
Add
</button>
</div>
{formData.tags.length > 0 && (
<div className="flex flex-wrap gap-6">
{formData.tags.map((tag) => (
<span
key={tag}
className="px-10 py-4 bg-heat-4 text-heat-100 rounded-6 text-body-small flex items-center gap-4"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="hover:text-heat-200"
>
<X className="w-12 h-12" />
</button>
</span>
))}
</div>
)}
</div>
{/* Difficulty & Time */}
<div className="grid grid-cols-2 gap-16">
<div>
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
<BarChart3 className="w-14 h-14" />
Difficulty
</label>
<select
value={formData.difficulty}
onChange={(e) => setFormData({ ...formData, difficulty: e.target.value })}
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black focus:outline-none focus:ring-2 focus:ring-heat-100"
>
{difficultyLevels.map((level) => (
<option key={level.value} value={level.value}>
{level.label}
</option>
))}
</select>
</div>
<div>
<label className="text-body-small font-medium text-accent-black mb-8 flex items-center gap-6">
<Clock className="w-14 h-14" />
Estimated Time
</label>
<input
type="text"
value={formData.estimatedTime}
onChange={(e) => setFormData({ ...formData, estimatedTime: e.target.value })}
placeholder="e.g., 5-10 minutes"
className="w-full px-12 py-10 bg-background-base border border-border-faint rounded-8 text-body-medium text-accent-black placeholder:text-black-alpha-32 focus:outline-none focus:ring-2 focus:ring-heat-100"
/>
</div>
</div>
{/* Visibility */}
<div className="p-16 bg-background-base rounded-8 border border-border-faint">
<div className="flex items-start gap-12">
<input
type="checkbox"
id="isPublic"
checked={formData.isPublic}
onChange={(e) => setFormData({ ...formData, isPublic: e.target.checked })}
className="mt-4 w-16 h-16 rounded-4 border-border-faint text-heat-100 focus:ring-heat-100"
/>
<label htmlFor="isPublic" className="flex-1 cursor-pointer">
<div className="flex items-center gap-8 mb-4">
{formData.isPublic ? (
<Globe className="w-16 h-16 text-heat-100" />
) : (
<Lock className="w-16 h-16 text-black-alpha-48" />
)}
<span className="text-body-medium font-medium text-accent-black">
{formData.isPublic ? "Public Template" : "Private Template"}
</span>
</div>
<p className="text-body-small text-black-alpha-48">
{formData.isPublic
? "This template will be visible to all users and can be used by anyone"
: "This template will only be visible to you"}
</p>
</label>
</div>
</div>
{/* Info Box */}
<div className="p-16 bg-heat-4 border border-heat-100 rounded-8">
<div className="flex items-start gap-12">
<Info className="w-16 h-16 text-heat-100 flex-shrink-0 mt-2" />
<div>
<p className="text-body-small text-accent-black">
Templates allow you to save and reuse workflow configurations. You can create
new workflows from your templates at any time.
</p>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="p-20 border-t border-border-faint bg-background-lighter flex justify-between">
<button
onClick={onClose}
className="px-20 py-10 bg-black-alpha-4 hover:bg-black-alpha-8 text-accent-black rounded-8 text-body-medium font-medium transition-all"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={isSaving || !formData.name.trim()}
className="px-20 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-body-medium font-medium transition-all active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-8"
>
{isSaving ? (
<>
<div className="w-16 h-16 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-16 h-16" />
Save as Template
</>
)}
</button>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}