Getting Started
Components
- Agent Status
- Agent Template Card
- API Key Card
- Audio Player
- Bar Visualizer
- Channel Status Card
- Chat Composer
- Code Editor
- Condition Builder
- Confirm Dialog
- Conversation
- Conversation Bar
- Dataset Card
- Document Status Badge
- Extraction Strategy Select
- File Icon
- File Upload
- Indexing Progress
- Live Waveform
- Matrix
- Message
- Mic Selector
- Model Diff View
- Model Selector
- Model Type Filter
- Node Catalog
- Option Editor
- Orb
- Output Variables View
- Package Card
- Prompt Editor
- Provider Status Card
- Radio Card
- Resource Card
- Resource Sidebar
- Response
- Run Node List
- Scrub Bar
- Segment Card
- Shimmering Text
- Speech Input
- Token Trend Chart
- Tool Call Card
- Transcript Viewer
- Usage Stat Card
- Variable Binding Editor
- Variable Selector
- Voice Button
- Voice Picker
- Waveform
- Workflow Node Card
- Workspace Selector
Support routing prompt
Compose prompts with reusable workflow variables.
25 words~34 tokens174 characters
"use client"
import { useState } from "react"
import { PromptEditor } from "@/components/ui/prompt-editor"
const initialPrompt = `You are a ZGI support agent.
Use {{customer.plan}} and {{conversation.intent}} to decide the next action.
If confidence is below 0.7, ask one concise clarification question.`
const variables = [
{ label: "Customer plan", value: "{{customer.plan}}" },
{ label: "Intent", value: "{{conversation.intent}}" },
{ label: "Transcript", value: "{{call.transcript}}" },
]
export function PromptEditorDemo() {
const [prompt, setPrompt] = useState(initialPrompt)
return (
<PromptEditor
value={prompt}
onValueChange={setPrompt}
title="Support routing prompt"
description="Compose prompts with reusable workflow variables."
variables={variables}
onRun={() => undefined}
className="w-full max-w-2xl"
/>
)
}
Installation
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/prompt-editor.jsonpnpm dlx shadcn@latest add https://ui.zgi.ai/r/prompt-editor.json
yarn dlx shadcn@latest add https://ui.zgi.ai/r/prompt-editor.jsonComponent
"use client"
import * as React from "react"
import {
BracesIcon,
CheckIcon,
CopyIcon,
PlayIcon,
WandSparklesIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
export interface PromptEditorVariable {
label: string
value: string
}
export interface PromptEditorProps
extends Omit<
React.ComponentProps<"textarea">,
"onChange" | "title" | "value"
> {
value: string
onValueChange?: (value: string) => void
title?: React.ReactNode
description?: React.ReactNode
variables?: PromptEditorVariable[]
onRun?: () => void
onImprove?: () => void
showStats?: boolean
editorClassName?: string
}
function countWords(value: string) {
return value.trim() ? value.trim().split(/\s+/).length : 0
}
export function PromptEditor({
value,
onValueChange,
title = "Prompt",
description,
variables = [],
onRun,
onImprove,
showStats = true,
disabled,
readOnly,
className,
editorClassName,
...props
}: PromptEditorProps) {
const [copied, setCopied] = React.useState(false)
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)
const wordCount = countWords(value)
const estimatedTokens = Math.ceil(wordCount * 1.35)
function insertVariable(variable: PromptEditorVariable) {
const textarea = textareaRef.current
const start = textarea?.selectionStart ?? value.length
const end = textarea?.selectionEnd ?? value.length
const next = `${value.slice(0, start)}${variable.value}${value.slice(end)}`
onValueChange?.(next)
window.requestAnimationFrame(() => {
textarea?.focus()
const cursor = start + variable.value.length
textarea?.setSelectionRange(cursor, cursor)
})
}
async function copyPrompt() {
await navigator.clipboard.writeText(value)
setCopied(true)
window.setTimeout(() => setCopied(false), 1200)
}
return (
<div
className={cn(
"border-border bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm",
className
)}
>
<div className="border-border flex items-start justify-between gap-3 border-b p-3">
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<WandSparklesIcon className="text-muted-foreground size-4 shrink-0" />
<p className="truncate text-sm font-medium">{title}</p>
</div>
{description ? (
<p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
{description}
</p>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1">
{onImprove ? (
<Button
type="button"
variant="ghost"
size="icon"
className="size-8 rounded-md"
disabled={disabled || readOnly}
onClick={onImprove}
aria-label="Improve prompt"
>
<WandSparklesIcon className="size-4" />
</Button>
) : null}
<Button
type="button"
variant="ghost"
size="icon"
className="size-8 rounded-md"
onClick={copyPrompt}
aria-label="Copy prompt"
>
{copied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)}
</Button>
{onRun ? (
<Button
type="button"
size="icon"
className="size-8 rounded-md"
disabled={disabled}
onClick={onRun}
aria-label="Run prompt"
>
<PlayIcon className="size-4" />
</Button>
) : null}
</div>
</div>
{variables.length ? (
<div className="border-border flex flex-wrap gap-1.5 border-b p-3">
{variables.map((variable) => (
<Button
key={`${variable.label}-${variable.value}`}
type="button"
variant="outline"
size="sm"
className="h-7 rounded-md px-2 text-xs"
disabled={disabled || readOnly}
onClick={() => insertVariable(variable)}
>
<BracesIcon className="size-3.5" />
{variable.label}
</Button>
))}
</div>
) : null}
<Textarea
ref={textareaRef}
value={value}
onChange={(event) => onValueChange?.(event.target.value)}
disabled={disabled}
readOnly={readOnly}
spellCheck={false}
className={cn(
"min-h-56 resize-y rounded-none border-0 bg-transparent font-mono text-sm shadow-none focus-visible:ring-0",
editorClassName
)}
{...props}
/>
{showStats ? (
<div className="border-border flex flex-wrap items-center gap-2 border-t px-3 py-2">
<Badge variant="secondary" className="rounded-sm">
{wordCount} words
</Badge>
<Badge variant="secondary" className="rounded-sm">
~{estimatedTokens} tokens
</Badge>
<span className="text-muted-foreground ml-auto text-xs">
{value.length} characters
</span>
</div>
) : null}
</div>
)
}
Usage
import { PromptEditor } from "@/components/ui/prompt-editor"
export function Example() {
return (
<PromptEditor
value="Use {{conversation.intent}} to route the user."
onValueChange={(value) => console.log(value)}
variables={[{ label: "Intent", value: "{{conversation.intent}}" }]}
/>
)
}Props
| Prop | Type | Default |
|---|---|---|
| value | string | required |
| onValueChange | (value: string) => void | - |
| title | ReactNode | "Prompt" |
| description | ReactNode | - |
| variables | PromptEditorVariable[] | [] |
| onRun | () => void | - |
| onImprove | () => void | - |
| showStats | boolean | true |
Deploy and Scale Agents with ZGI
ZGI delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert