open-prompt / src /app /workflows /page.tsx
GitHub Action
Automated sync to Hugging Face
bcce530
"use client"
import { useState } from "react"
import Link from "next/link"
import { useUser } from "@stackframe/stack"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { ModelSelector } from "@/components/model-selector"
import { DEFAULT_MODEL } from "@/types/prompt"
import {
Workflow,
Plus,
Play,
ArrowRight,
Trash2,
ChevronDown,
ChevronUp,
Loader2,
Copy,
Check,
Sparkles,
PenLine,
Search,
Share2,
Save,
Globe,
Library,
Lightbulb,
Repeat
} from "lucide-react"
import ReactMarkdown from "react-markdown"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
interface WorkflowStep {
id: string
name: string
prompt: string
output?: string
isLoading?: boolean
iterations?: number // Loop count (1 = run once, 2+ = repeat)
}
// Preset workflow templates
const WORKFLOW_TEMPLATES = [
{
name: "Blog Post Creator",
description: "Generate a complete blog post from a topic",
steps: [
{ name: "Topic Research", prompt: "Research and outline key points about: {{topic}}" },
{ name: "Write Draft", prompt: "Using this research:\n{{previous_output}}\n\nWrite a detailed blog post about {{topic}}" },
{ name: "SEO Optimize", prompt: "Optimize this content for SEO:\n{{previous_output}}\n\nAdd meta description, keywords, and improve headings." },
]
},
{
name: "Content Repurposer",
description: "Turn one piece of content into multiple formats",
steps: [
{ name: "Summarize", prompt: "Summarize the key points from:\n{{input}}" },
{ name: "Twitter Thread", prompt: "Turn this into a Twitter thread (5-7 tweets):\n{{previous_output}}" },
{ name: "LinkedIn Post", prompt: "Create a LinkedIn post from this:\n{{previous_output}}" },
]
},
{
name: "Product Launch",
description: "Create launch materials for a product",
steps: [
{ name: "Product Description", prompt: "Write a compelling product description for: {{product}}" },
{ name: "Email Announcement", prompt: "Create a launch email based on:\n{{previous_output}}" },
{ name: "Social Post", prompt: "Create social media posts for this launch:\n{{previous_output}}" },
]
},
]
export default function WorkflowsPage() {
const user = useUser()
const [steps, setSteps] = useState<WorkflowStep[]>([])
const [workflowName, setWorkflowName] = useState("")
const [workflowDescription, setWorkflowDescription] = useState("")
const [isRunning, setIsRunning] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [savedSlug, setSavedSlug] = useState<string | null>(null)
const [currentStepIndex, setCurrentStepIndex] = useState(-1)
const [variables, setVariables] = useState<Record<string, string>>({})
const [copied, setCopied] = useState(false)
const [visibility, setVisibility] = useState<"private" | "public">("private")
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_MODEL)
// Add a new step
const addStep = () => {
const newStep: WorkflowStep = {
id: `step-${Date.now()}`,
name: `Step ${steps.length + 1}`,
prompt: "",
iterations: 1,
}
setSteps([...steps, newStep])
}
// Remove a step
const removeStep = (id: string) => {
setSteps(steps.filter(s => s.id !== id))
}
// Update step
const updateStep = (id: string, updates: Partial<WorkflowStep>) => {
setSteps(steps.map(s => s.id === id ? { ...s, ...updates } : s))
}
// Move step up/down
const moveStep = (index: number, direction: "up" | "down") => {
const newIndex = direction === "up" ? index - 1 : index + 1
if (newIndex < 0 || newIndex >= steps.length) return
const newSteps = [...steps]
;[newSteps[index], newSteps[newIndex]] = [newSteps[newIndex], newSteps[index]]
setSteps(newSteps)
}
// Load template
const loadTemplate = (templateIndex: number) => {
const template = WORKFLOW_TEMPLATES[templateIndex]
setWorkflowName(template.name)
setSteps(template.steps.map((step, i) => ({
id: `step-${Date.now()}-${i}`,
name: step.name,
prompt: step.prompt,
})))
// Extract variables from prompts
const allPrompts = template.steps.map(s => s.prompt).join(" ")
const varMatches = allPrompts.match(/\{\{(\w+)\}\}/g) || []
const uniqueVars = [...new Set(varMatches.map(v => v.replace(/\{\{|\}\}/g, "")))]
.filter(v => v !== "previous_output")
const newVars: Record<string, string> = {}
uniqueVars.forEach(v => { newVars[v] = "" })
setVariables(newVars)
}
// Run the workflow
const runWorkflow = async () => {
if (steps.length === 0) return
setIsRunning(true)
let previousOutput = ""
for (let i = 0; i < steps.length; i++) {
const iterations = steps[i].iterations || 1
for (let iter = 0; iter < iterations; iter++) {
setCurrentStepIndex(i)
setSteps(prev => prev.map((s, idx) =>
idx === i ? { ...s, isLoading: true, output: iterations > 1 ? `[Iteration ${iter + 1}/${iterations}]\n` : "" } : s
))
// Replace variables in prompt
let processedPrompt = steps[i].prompt
Object.entries(variables).forEach(([key, value]) => {
processedPrompt = processedPrompt.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value)
})
processedPrompt = processedPrompt.replace(/\{\{previous_output\}\}/g, previousOutput)
processedPrompt = processedPrompt.replace(/\{\{iteration\}\}/g, String(iter + 1))
try {
const response = await fetch("/api/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: processedPrompt,
model: selectedModel,
}),
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let fullOutput = iterations > 1 ? `[Iteration ${iter + 1}/${iterations}]\n` : ""
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
fullOutput += chunk
setSteps(prev => prev.map((s, idx) =>
idx === i ? { ...s, output: fullOutput } : s
))
}
}
previousOutput = fullOutput
setSteps(prev => prev.map((s, idx) =>
idx === i ? { ...s, isLoading: false } : s
))
} catch (error) {
setSteps(prev => prev.map((s, idx) =>
idx === i ? { ...s, isLoading: false, output: "Error: Failed to run step" } : s
))
break
}
}
}
setIsRunning(false)
setCurrentStepIndex(-1)
}
// Save workflow to database
const saveWorkflow = async () => {
if (!workflowName.trim() || steps.length === 0) return
setIsSaving(true)
try {
const response = await fetch("/api/workflows", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: workflowName,
description: workflowDescription,
steps: steps.map(s => ({ name: s.name, prompt: s.prompt })),
variables: Object.keys(variables),
visibility,
creatorId: user?.id,
}),
})
if (response.ok) {
const data = await response.json()
setSavedSlug(data.slug)
}
} catch (error) {
console.error("Failed to save workflow:", error)
} finally {
setIsSaving(false)
}
}
// Copy all outputs
const copyAllOutputs = async () => {
const allOutputs = steps
.filter(s => s.output)
.map(s => `## ${s.name}\n\n${s.output}`)
.join("\n\n---\n\n")
await navigator.clipboard.writeText(allOutputs)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="container mx-auto px-4 py-8 max-w-6xl">
{/* Hero Section */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center h-16 w-16 rounded-2xl bg-linear-to-br from-primary to-accent mb-4">
<Workflow className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl md:text-5xl font-serif font-medium mb-4">
AI <span className="text-gradient italic">Workflows</span>
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Chain multiple prompts together. Output from one step becomes input to the next.
</p>
</div>
{/* Templates */}
<div className="mb-8">
<h2 className="text-lg font-semibold mb-4">Quick Start Templates</h2>
<div className="grid md:grid-cols-3 gap-4">
{WORKFLOW_TEMPLATES.map((template, index) => (
<Card
key={template.name}
className="cursor-pointer hover:border-primary transition-colors"
onClick={() => loadTemplate(index)}
>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-5 w-5 text-primary" />
<h3 className="font-medium">{template.name}</h3>
</div>
<p className="text-sm text-muted-foreground mb-3">
{template.description}
</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Badge variant="outline">{template.steps.length} steps</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</div>
<div className="grid lg:grid-cols-3 gap-6">
{/* Workflow Builder */}
<div className="lg:col-span-2 space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Workflow Builder</CardTitle>
<CardDescription>Create your prompt chain</CardDescription>
</div>
<Button onClick={addStep} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
Add Step
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Workflow Name & Description */}
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label>Workflow Name</Label>
<Input
value={workflowName}
onChange={(e) => setWorkflowName(e.target.value)}
placeholder="My Custom Workflow"
/>
</div>
<div>
<Label>Description (optional)</Label>
<Input
value={workflowDescription}
onChange={(e) => setWorkflowDescription(e.target.value)}
placeholder="What does this workflow do?"
/>
</div>
</div>
{/* Steps */}
{steps.length === 0 ? (
<Card className="border-dashed">
<CardContent className="p-8 text-center text-muted-foreground">
<Workflow className="h-12 w-12 mx-auto mb-4 opacity-20" />
<p>No steps yet. Add a step or select a template to get started.</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{steps.map((step, index) => (
<Card key={step.id} className={step.isLoading ? "border-primary" : ""}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge variant="outline" className="h-8 w-8 rounded-full flex items-center justify-center">
{index + 1}
</Badge>
<Input
value={step.name}
onChange={(e) => updateStep(step.id, { name: e.target.value })}
className="font-medium w-48"
/>
{step.isLoading && (
<Loader2 className="h-4 w-4 animate-spin text-primary" />
)}
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => moveStep(index, "up")}
disabled={index === 0}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => moveStep(index, "down")}
disabled={index === steps.length - 1}
>
<ChevronDown className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => removeStep(step.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-4">
<div className="flex-1">
<Label className="text-sm">Prompt</Label>
<Textarea
value={step.prompt}
onChange={(e) => updateStep(step.id, { prompt: e.target.value })}
placeholder="Enter prompt... Use {{variable}} for inputs, {{previous_output}} for chaining, {{iteration}} for loop count"
rows={3}
/>
</div>
<div className="w-24">
<Label className="text-sm flex items-center gap-1">
<Repeat className="h-3 w-3" />
Loop
</Label>
<Select
value={String(step.iterations || 1)}
onValueChange={(val) => updateStep(step.id, { iterations: parseInt(val) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 5].map(n => (
<SelectItem key={n} value={String(n)}>
{n}x
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{step.output && (
<div>
<Label className="text-sm">Output</Label>
<div className="max-h-40 overflow-auto rounded-lg border bg-muted/30 p-3">
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{step.output}</ReactMarkdown>
</div>
</div>
</div>
)}
</CardContent>
{index < steps.length - 1 && (
<div className="flex justify-center py-2">
<ArrowRight className="h-5 w-5 text-muted-foreground" />
</div>
)}
</Card>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Variables & Actions */}
<div className="space-y-4">
{/* Variables */}
{Object.keys(variables).length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Variables</CardTitle>
<CardDescription>Fill in your workflow inputs</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{Object.entries(variables).map(([key, value]) => (
<div key={key}>
<Label className="capitalize">{key.replace(/_/g, " ")}</Label>
<Input
value={value}
onChange={(e) => setVariables(prev => ({ ...prev, [key]: e.target.value }))}
placeholder={`Enter ${key}`}
/>
</div>
))}
</CardContent>
</Card>
)}
{/* Model Selection */}
<Card>
<CardHeader>
<CardTitle className="text-lg">AI Model</CardTitle>
<CardDescription>Select which model to use</CardDescription>
</CardHeader>
<CardContent>
<ModelSelector
value={selectedModel}
onChange={setSelectedModel}
/>
</CardContent>
</Card>
{/* Actions */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
className="w-full gap-2"
onClick={runWorkflow}
disabled={steps.length === 0 || isRunning}
>
{isRunning ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Running Step {currentStepIndex + 1}/{steps.length}...
</>
) : (
<>
<Play className="h-4 w-4" />
Run Workflow
</>
)}
</Button>
{/* Save Workflow */}
<div className="pt-2 border-t space-y-2">
<div className="flex gap-2">
<Button
variant={visibility === "private" ? "default" : "outline"}
size="sm"
onClick={() => setVisibility("private")}
className="flex-1"
>
Private
</Button>
<Button
variant={visibility === "public" ? "default" : "outline"}
size="sm"
onClick={() => setVisibility("public")}
className="flex-1 gap-1"
>
<Globe className="h-3 w-3" />
Public
</Button>
</div>
<Button
variant="outline"
className="w-full gap-2"
onClick={saveWorkflow}
disabled={!workflowName.trim() || steps.length === 0 || isSaving}
>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving...
</>
) : savedSlug ? (
<>
<Check className="h-4 w-4" />
Saved!
</>
) : (
<>
<Save className="h-4 w-4" />
Save Workflow
</>
)}
</Button>
</div>
{steps.some(s => s.output) && (
<Button
variant="outline"
className="w-full gap-2"
onClick={copyAllOutputs}
>
{copied ? (
<>
<Check className="h-4 w-4" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy All Outputs
</>
)}
</Button>
)}
<Link href="/workflow-library">
<Button variant="ghost" className="w-full gap-2">
<Library className="h-4 w-4" />
Browse Library
</Button>
</Link>
</CardContent>
</Card>
{/* Tips */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2"><Lightbulb className="h-5 w-5 text-yellow-500" /> Tips</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>• Use <code className="text-primary">{`{{previous_output}}`}</code> to chain outputs</p>
<p>• Use <code className="text-primary">{`{{variable}}`}</code> for custom inputs</p>
<p>• Reorder steps with ↑↓ buttons</p>
<p>• Each step runs after the previous completes</p>
</CardContent>
</Card>
</div>
</div>
</div>
)
}