Spaces:
Paused
Paused
| import React, { useState, useEffect } from 'react'; | |
| import { Play, Plus, Trash2, Save, Rocket, Clock, FileText } from 'lucide-react'; | |
| import { Workflow, WorkflowStep, Note } from '../types'; | |
| import { STORAGE_KEYS, DEFAULT_WORKFLOWS } from '../constants'; | |
| interface WorkflowPanelProps { | |
| templates: Note[]; | |
| onStartWorkflow: (workflow: Workflow) => void; | |
| } | |
| export const WorkflowPanel: React.FC<WorkflowPanelProps> = ({ templates, onStartWorkflow }) => { | |
| const [workflows, setWorkflows] = useState<Workflow[]>(() => { | |
| const stored = localStorage.getItem(STORAGE_KEYS.WORKFLOWS); | |
| return stored ? JSON.parse(stored) : DEFAULT_WORKFLOWS; | |
| }); | |
| const [isEditing, setIsEditing] = useState(false); | |
| const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null); | |
| useEffect(() => { | |
| localStorage.setItem(STORAGE_KEYS.WORKFLOWS, JSON.stringify(workflows)); | |
| }, [workflows]); | |
| const handleCreateNew = () => { | |
| setEditingWorkflow({ | |
| id: `workflow-${Date.now()}`, | |
| name: 'New Workflow', | |
| description: '', | |
| steps: [{ templateId: '', delayMinutes: 0 }] | |
| }); | |
| setIsEditing(true); | |
| }; | |
| const handleSave = () => { | |
| if (!editingWorkflow) return; | |
| if (!editingWorkflow.name.trim()) { | |
| alert('Workflow name is required.'); | |
| return; | |
| } | |
| if (editingWorkflow.steps.some(s => !s.templateId)) { | |
| alert('All steps must have a selected template.'); | |
| return; | |
| } | |
| setWorkflows(prev => { | |
| const existingIndex = prev.findIndex(w => w.id === editingWorkflow.id); | |
| if (existingIndex >= 0) { | |
| const newWorkflows = [...prev]; | |
| newWorkflows[existingIndex] = editingWorkflow; | |
| return newWorkflows; | |
| } | |
| return [...prev, editingWorkflow]; | |
| }); | |
| setIsEditing(false); | |
| setEditingWorkflow(null); | |
| }; | |
| const handleDelete = (id: string) => { | |
| if (confirm('Delete this workflow?')) { | |
| setWorkflows(prev => prev.filter(w => w.id !== id)); | |
| } | |
| }; | |
| const addStep = () => { | |
| if (editingWorkflow) { | |
| setEditingWorkflow({ | |
| ...editingWorkflow, | |
| steps: [...editingWorkflow.steps, { templateId: '', delayMinutes: 30 }] | |
| }); | |
| } | |
| }; | |
| const removeStep = (index: number) => { | |
| if (editingWorkflow && editingWorkflow.steps.length > 1) { | |
| const newSteps = [...editingWorkflow.steps]; | |
| newSteps.splice(index, 1); | |
| setEditingWorkflow({ ...editingWorkflow, steps: newSteps }); | |
| } | |
| }; | |
| const updateStep = (index: number, field: keyof WorkflowStep, value: string | number) => { | |
| if (editingWorkflow) { | |
| const newSteps = [...editingWorkflow.steps]; | |
| newSteps[index] = { ...newSteps[index], [field]: value }; | |
| setEditingWorkflow({ ...editingWorkflow, steps: newSteps }); | |
| } | |
| }; | |
| return ( | |
| <div className="flex flex-col h-full bg-white p-6 overflow-y-auto"> | |
| <div className="max-w-3xl mx-auto w-full space-y-6"> | |
| <div className="flex justify-between items-center"> | |
| <div> | |
| <h2 className="text-2xl font-bold flex items-center gap-2 text-gray-900"> | |
| <Rocket className="w-6 h-6 text-indigo-600" /> | |
| Workflow Builder | |
| </h2> | |
| <p className="text-gray-500 mt-1 text-sm">Create sequences of templates that fire on a timer.</p> | |
| </div> | |
| {!isEditing && ( | |
| <button | |
| onClick={handleCreateNew} | |
| className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-xl text-sm font-bold flex items-center gap-2 transition-all" | |
| > | |
| <Plus className="w-4 h-4" /> New Workflow | |
| </button> | |
| )} | |
| </div> | |
| {isEditing && editingWorkflow ? ( | |
| <div className="bg-gray-50 border border-gray-200 rounded-2xl p-6 space-y-6 animate-in fade-in slide-in-from-top-4"> | |
| <div className="flex justify-between items-center border-b border-gray-200 pb-4"> | |
| <h3 className="text-lg font-bold text-gray-800">Edit Workflow</h3> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={() => { setIsEditing(false); setEditingWorkflow(null); }} | |
| className="px-4 py-2 text-gray-500 hover:bg-gray-200 rounded-xl text-sm font-bold transition-all" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleSave} | |
| className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-xl text-sm font-bold flex items-center gap-2 transition-all shadow-sm" | |
| > | |
| <Save className="w-4 h-4" /> Save Workflow | |
| </button> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-600 uppercase tracking-wider mb-1">Workflow Name</label> | |
| <input | |
| type="text" | |
| value={editingWorkflow.name} | |
| onChange={(e) => setEditingWorkflow({ ...editingWorkflow, name: e.target.value })} | |
| className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none transition-all" | |
| placeholder="e.g. Code Review & Deploy" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-bold text-gray-600 uppercase tracking-wider mb-1">Description (Optional)</label> | |
| <input | |
| type="text" | |
| value={editingWorkflow.description} | |
| onChange={(e) => setEditingWorkflow({ ...editingWorkflow, description: e.target.value })} | |
| className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none transition-all text-sm" | |
| placeholder="What does this sequence do?" | |
| /> | |
| </div> | |
| <div className="pt-4 border-t border-gray-200"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <label className="text-xs font-bold text-gray-800 uppercase tracking-wider">Execution Steps</label> | |
| </div> | |
| <div className="space-y-4"> | |
| {editingWorkflow.steps.map((step, idx) => ( | |
| <div key={idx} className="flex gap-3 bg-white p-4 rounded-xl border border-gray-200 shadow-sm relative"> | |
| <div className="flex flex-col items-center justify-center bg-indigo-50 text-indigo-700 font-bold rounded-lg w-8 h-8 flex-shrink-0"> | |
| {idx + 1} | |
| </div> | |
| <div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-[10px] font-bold text-gray-500 uppercase tracking-wider mb-1 flex items-center gap-1"> | |
| <FileText className="w-3 h-3" /> Select Template | |
| </label> | |
| <select | |
| value={step.templateId} | |
| onChange={(e) => updateStep(idx, 'templateId', e.target.value)} | |
| className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm outline-none focus:ring-2 focus:ring-indigo-500 bg-white" | |
| > | |
| <option value="">-- Choose Template --</option> | |
| {templates.map(t => ( | |
| <option key={t.id} value={t.id}>{t.title || t.id}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-[10px] font-bold text-gray-500 uppercase tracking-wider mb-1 flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> Delay Before Firing (Mins) | |
| </label> | |
| <input | |
| type="number" | |
| min="0" | |
| value={step.delayMinutes} | |
| onChange={(e) => updateStep(idx, 'delayMinutes', parseInt(e.target.value) || 0)} | |
| disabled={idx === 0} | |
| className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-gray-100 disabled:text-gray-400" | |
| /> | |
| </div> | |
| </div> | |
| {idx > 0 && ( | |
| <button | |
| onClick={() => removeStep(idx)} | |
| className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all absolute right-2 top-2" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| <button | |
| onClick={addStep} | |
| className="mt-4 flex items-center gap-1 text-sm font-bold text-indigo-600 hover:text-indigo-700 bg-indigo-50 hover:bg-indigo-100 px-3 py-2 rounded-lg transition-colors" | |
| > | |
| <Plus className="w-4 h-4" /> Add Step | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {workflows.map(wf => ( | |
| <div key={wf.id} className="bg-white border border-gray-200 rounded-2xl p-5 hover:border-indigo-300 transition-all shadow-sm hover:shadow-md flex flex-col group relative"> | |
| {wf.id !== 'hf-deployment-cue' && ( | |
| <button | |
| onClick={() => handleDelete(wf.id)} | |
| className="absolute top-4 right-4 p-2 text-gray-400 hover:text-red-500 bg-gray-50 hover:bg-red-50 rounded-lg transition-all opacity-0 group-hover:opacity-100" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| )} | |
| <div className="flex-1"> | |
| <h3 className="text-lg font-bold text-gray-900 mb-1 flex items-center gap-2"> | |
| {wf.name} | |
| {wf.id === 'hf-deployment-cue' && <span className="bg-indigo-100 text-indigo-700 text-[10px] px-2 py-0.5 rounded-full uppercase tracking-wider">System</span>} | |
| </h3> | |
| <p className="text-sm text-gray-500 mb-4">{wf.description || 'No description provided.'}</p> | |
| <div className="space-y-2 mb-6"> | |
| <div className="text-xs font-bold text-gray-400 uppercase tracking-wider border-b border-gray-100 pb-1 mb-2">Sequence</div> | |
| {wf.steps.map((step, idx) => ( | |
| <div key={idx} className="flex items-center text-xs text-gray-600"> | |
| <span className="w-5 font-bold text-gray-400">{idx + 1}.</span> | |
| <span className="font-medium truncate flex-1">{templates.find(t => t.id === step.templateId)?.title || step.templateId}</span> | |
| {step.delayMinutes > 0 && ( | |
| <span className="text-indigo-500 flex items-center gap-1 font-bold bg-indigo-50 px-1.5 py-0.5 rounded ml-2 whitespace-nowrap"> | |
| <Clock className="w-3 h-3" /> +{step.delayMinutes}m | |
| </span> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={() => onStartWorkflow(wf)} | |
| className="flex-1 bg-indigo-50 hover:bg-indigo-600 text-indigo-700 hover:text-white py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all shadow-sm group/btn" | |
| > | |
| <Play className="w-4 h-4 fill-current opacity-70 group-hover/btn:opacity-100" /> Start Workflow | |
| </button> | |
| <button | |
| onClick={() => { | |
| setEditingWorkflow(wf); | |
| setIsEditing(true); | |
| }} | |
| className="px-4 py-2.5 bg-white border border-gray-200 hover:bg-gray-50 text-gray-600 rounded-xl text-sm font-bold transition-colors" | |
| > | |
| Edit | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| {workflows.length === 0 && ( | |
| <div className="col-span-full py-12 flex flex-col items-center justify-center text-gray-400 border-2 border-dashed border-gray-200 rounded-2xl"> | |
| <Rocket className="w-12 h-12 mb-3 text-gray-300" /> | |
| <p>No workflows created yet.</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |