Spaces:
Configuration error
Configuration error
| "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> | |
| ) | |
| } | |