jules / components /WorkflowPanel.tsx
GraziePrego's picture
Upload folder using huggingface_hub
34450be verified
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>
);
};