"use client" import { useState, useCallback, useMemo } from "react" import { useUser } from "@stackframe/stack" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input, Textarea } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import { extractVariables, generateSlug } from "@/lib/utils" import { VariableSchema, PromptSchema, AI_MODELS, ModelId, DEFAULT_MODEL } from "@/types/prompt" import { useOllama } from "@/contexts/ollama-context" import { FrameworkSelector } from "@/components/create/framework-selector" import { FrameworkScaffold } from "@/components/create/framework-scaffold" import { PromptCoach } from "@/components/editor/prompt-coach" import { getFramework, generateFromFramework, Framework } from "@/lib/frameworks" import { Sparkles, Play, Save, Eye, Code, Plus, Trash2, GripVertical, ChevronDown, AlertCircle, Target } from "lucide-react" import { cn } from "@/lib/utils" type VariableType = "text" | "textarea" | "dropdown" | "number" | "boolean" const VARIABLE_TYPES: { value: VariableType; label: string }[] = [ { value: "text", label: "Text Input" }, { value: "textarea", label: "Text Area" }, { value: "dropdown", label: "Dropdown" }, { value: "number", label: "Number" }, { value: "boolean", label: "Checkbox" }, ] const CATEGORIES = [ "Content", "Development", "Marketing", "Business", "Education", "Creative", "Research", "Other", ] interface EditorState { title: string description: string template: string category: string tags: string modelDefault: string visibility: "public" | "unlisted" | "private" } interface PromptEditorProps { initialData?: { title: string description: string | null template: string category: string | null tags: string[] modelDefault: string visibility: string schema: PromptSchema | null } promptSlug?: string // If provided, we're editing an existing prompt } export function PromptEditor({ initialData, promptSlug }: PromptEditorProps = {}) { const user = useUser() const { settings: ollamaSettings } = useOllama() const [state, setState] = useState({ title: initialData?.title || "", description: initialData?.description || "", template: initialData?.template || "", category: initialData?.category || "", tags: initialData?.tags?.join(", ") || "", modelDefault: initialData?.modelDefault || DEFAULT_MODEL, visibility: (initialData?.visibility as EditorState["visibility"]) || "public", }) // Initialize variables from initialData if editing const initialVariables = initialData?.schema?.variables || [] const [variables, setVariables] = useState(initialVariables) const [isPreview, setIsPreview] = useState(false) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) // Framework Engine state const [selectedFramework, setSelectedFramework] = useState(null) const [frameworkSections, setFrameworkSections] = useState>({}) const [showFrameworkSelector, setShowFrameworkSelector] = useState(true) // Get the selected framework object const currentFramework = useMemo(() => { return selectedFramework ? getFramework(selectedFramework) : null }, [selectedFramework]) // Handle framework selection const handleFrameworkSelect = (frameworkId: string | null) => { setSelectedFramework(frameworkId) setFrameworkSections({}) if (!frameworkId) { // Clear template if deselecting framework setShowFrameworkSelector(false) } else { setShowFrameworkSelector(false) } } // Generate template from framework sections const handleGenerateFromFramework = useCallback(() => { if (currentFramework) { const generatedTemplate = generateFromFramework(currentFramework, frameworkSections) setState(s => ({ ...s, template: generatedTemplate })) } }, [currentFramework, frameworkSections]) // Auto-detect variables from template const detectedVariables = useMemo(() => { return extractVariables(state.template) }, [state.template]) // Sync detected variables with current variable configs const handleDetectVariables = useCallback(() => { const newVariables = detectedVariables.map((name) => { // Keep existing config if variable already exists const existing = variables.find((v) => v.name === name) if (existing) return existing return { name, type: "text" as const, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " "), placeholder: `Enter ${name.replace(/_/g, " ")}...`, required: true, } }) setVariables(newVariables) }, [detectedVariables, variables]) // Update a variable config const updateVariable = (index: number, updates: Partial) => { setVariables((prev) => prev.map((v, i) => (i === index ? { ...v, ...updates } : v)) ) } // Handle form submission const handleSave = async (publish: boolean = false) => { setError(null) if (!state.title.trim()) { setError("Title is required") return } if (!state.template.trim()) { setError("Prompt template is required") return } setIsSaving(true) try { const schema: PromptSchema = { variables, output: { format: "markdown", streaming: true, }, } const isEditing = !!promptSlug const url = isEditing ? `/api/prompts/${promptSlug}` : "/api/prompts" const method = isEditing ? "PUT" : "POST" const response = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: state.title, description: state.description, template: state.template, schema, category: state.category || null, tags: state.tags.split(",").map((t) => t.trim()).filter(Boolean), modelDefault: state.modelDefault, visibility: publish ? state.visibility : "private", ...(isEditing ? {} : {}), // creatorId is now handled server-side via auth }), }) if (!response.ok) { throw new Error(isEditing ? "Failed to update prompt" : "Failed to save prompt") } const data = await response.json() // Redirect to the prompt page window.location.href = `/p/${data.slug || promptSlug}` } catch (err) { setError(err instanceof Error ? err.message : "Failed to save prompt") } finally { setIsSaving(false) } } return (
{/* Left Pane - Template Editor */}
{/* Framework Selector */} {showFrameworkSelector && ( )} {/* Framework Scaffold - when a framework is selected */} {currentFramework && !showFrameworkSelector && (
Using {currentFramework.name} Framework
)} {/* Button to show framework selector if hidden and no framework selected */} {!showFrameworkSelector && !currentFramework && ( )}
Prompt Template {selectedFramework && ( {selectedFramework} )}
Use {"{{variable_name}}"} syntax to create input fields
setState((s) => ({ ...s, title: e.target.value }))} className="text-lg font-medium" />