Prompt Playground
blockaiAI model prompt engineering studio & parameter tuner with system instructions, user input, temperature, max tokens, top-p sliders, JSON/stream modes, and interactive response evaluation.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/prompt-playground.json$npx shadcn@latest add https://uipkge.dev/r/react/prompt-playground.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/prompt-playground.json$bunx shadcn@latest add https://uipkge.dev/r/react/prompt-playground.jsonNamed registry:
npx shadcn@latest add @uipkge-react/prompt-playgroundInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ModelOptioninterface ModelOption {
id: string
name: string
provider: string
contextWindow: string
inputCostPer1M: number
outputCostPer1M: number
}Presetinterface Preset {
id: string
name: string
description: string
model: string
temperature: number
maxTokens: number
topP: number
jsonMode: boolean
stream: boolean
systemPrompt: string
userMessage: string
mockResponse: string
mockTokens: number
mockLatency: string
}Files installed (1)
components/blocks/PromptPlayground.tsx30.7 kB
'use client' import * as React from 'react' import { Bot, Braces, Check, Clock, Copy, Cpu, FileCode, MessageSquare, RotateCcw, Save, Sliders, Sparkles, Terminal, Zap, } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Slider } from '@/components/ui/slider' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' interface ModelOption { id: string name: string provider: string contextWindow: string inputCostPer1M: number outputCostPer1M: number } interface Preset { id: string name: string description: string model: string temperature: number maxTokens: number topP: number jsonMode: boolean stream: boolean systemPrompt: string userMessage: string mockResponse: string mockTokens: number mockLatency: string } const models: ModelOption[] = [ { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', provider: 'Anthropic', contextWindow: '200k', inputCostPer1M: 3.0, outputCostPer1M: 15.0, }, { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI', contextWindow: '128k', inputCostPer1M: 2.5, outputCostPer1M: 10.0, }, { id: 'llama-3-3-70b', name: 'Llama 3.3 70B', provider: 'Meta', contextWindow: '128k', inputCostPer1M: 0.6, outputCostPer1M: 0.8, }, ] const presets: Preset[] = [ { id: 'code-reviewer', name: 'Code Reviewer', description: 'Security & vulnerability audit for backend code', model: 'claude-3-5-sonnet', temperature: 0.2, maxTokens: 2048, topP: 0.9, jsonMode: false, stream: true, systemPrompt: 'You are a world-class senior developer assistant specializing in application security, code reviews, and high-performance software architecture. Analyze code with precision, highlight vulnerabilities (OWASP Top 10), and provide clean refactored snippets.', userMessage: 'Review the following authentication middleware for security vulnerabilities and race conditions:\n\n```typescript\nexport async function authMiddleware(req: Request) {\n const token = req.headers.get("authorization")?.split(" ")[1];\n if (!token) throw new Error("Unauthorized");\n const session = await db.session.findUnique({ where: { token } });\n if (session.expiresAt < new Date()) {\n await db.session.delete({ where: { token } });\n throw new Error("Expired");\n }\n return session.user;\n}\n```', mockResponse: '### Security Review & Vulnerability Analysis\n\nI identified 3 key security & stability issues in the provided authentication middleware:\n\n1. **Unhandled Exceptions / Missing Error Envelope:**\n Throwing raw `Error` instances inside middleware can cause unhandled 500 crashes instead of structured 401/403 HTTP responses.\n2. **Missing Token Existence Check on DB lookup:**\n If `session` is not found, `session.expiresAt` throws a `TypeError` (cannot read properties of null).\n3. **Non-atomic Cleanup & Database Mutation:**\n Deleting expired tokens in the read path introduces write amplification on every expired request and isn\'t atomic with the expiration verification.\n\n---\n\n### Recommended Refactoring\n\n```typescript\nimport { NextResponse } from "next/server";\n\nexport async function authMiddleware(req: Request) {\n const authHeader = req.headers.get("authorization");\n if (!authHeader?.startsWith("Bearer ")) {\n return new Response(JSON.stringify({ error: "Missing or malformed Authorization header" }), {\n status: 401,\n headers: { "Content-Type": "application/json" },\n });\n }\n\n const token = authHeader.slice(7).trim();\n const session = await db.session.findUnique({\n where: { token },\n include: { user: true },\n });\n\n if (!session || session.expiresAt.getTime() <= Date.now()) {\n return new Response(JSON.stringify({ error: "Invalid or expired session" }), {\n status: 401,\n headers: { "Content-Type": "application/json" },\n });\n }\n\n return session.user;\n}\n```\n\n**Key Improvements:**\n- Constant-time prefix check avoiding unexpected array indexing errors.\n- Null-safe session expiration check without throwing uncaught exceptions.\n- Immediate 401 JSON error responses matching standard API conventions.', mockTokens: 428, mockLatency: '1.4s', }, { id: 'customer-support', name: 'Customer Support Agent', description: 'Empathetic, brand-aligned troubleshooting for customer inquiries', model: 'gpt-4o', temperature: 0.7, maxTokens: 1024, topP: 0.95, jsonMode: false, stream: true, systemPrompt: 'You are an empathetic, concise, and professional Tier-2 support specialist for CloudFlow SaaS. Your goal is to troubleshoot customer issues efficiently while maintaining a helpful, brand-aligned tone.', userMessage: 'A customer wrote: "Our webhooks stopped delivering payload events since 14:00 UTC today. We are seeing intermittent 504 gateway timeout errors on endpoint https://api.client.com/events. Can you check our account ID #ACC-9821?"', mockResponse: 'Hello Team,\n\nThank you for reaching out, and I apologize for the disruption to your event pipeline.\n\nI investigated account **#ACC-9821** and observed the following:\n\n- **Root Cause:** Our webhook dispatch worker encountered downstream 504 Gateway Timeouts from `https://api.client.com/events` when response times exceeded the 5,000ms threshold.\n- **Retry Mechanism:** Failed webhook events have been automatically enrolled into exponential backoff (attempt 3 of 5).\n- **Action Taken:** We temporarily increased the worker handshake timeout to 8,000ms for your tenant while your ingress recovers.\n\n**Recommended Next Steps:**\n1. Check your upstream server logs around 14:00 UTC for concurrent connection saturation or database connection pool starvation.\n2. If you need all 14:00–15:00 UTC events replayed immediately, reply with **"CONFIRM REPLAY #ACC-9821"** and I will trigger a zero-loss backfill.\n\nBest regards,\n**CloudFlow Tier-2 Engineering Support**', mockTokens: 286, mockLatency: '0.9s', }, { id: 'json-extractor', name: 'JSON Extractor', description: 'Strict schema extraction from unstructured invoices or logs', model: 'gpt-4o', temperature: 0.0, maxTokens: 1024, topP: 0.1, jsonMode: true, stream: false, systemPrompt: 'You are an infallible data extraction engine. Extract structured entities from unstructured receipts, invoices, or logs and return ONLY valid RFC-8259 JSON matching the requested schema. Do not include markdown formatting, explanations, or prologue.', userMessage: 'Extract invoice metadata:\nInvoice Number: INV-2026-8891\nDate: August 21, 2026\nVendor: Acme Cloud Services LLC (Tax ID: US-99182312)\nClient: Cyberdyne Systems\nItems:\n1. GPU Cluster Instance H100 x 48 hrs @ $3.20/hr = $153.60\n2. NVMe High-Speed Storage 2TB x 1 mo @ $80.00/mo = $80.00\nTax: $18.69 (8%)\nTotal Due: $252.29', mockResponse: '{\n "invoiceNumber": "INV-2026-8891",\n "issueDate": "2026-08-21",\n "vendor": {\n "name": "Acme Cloud Services LLC",\n "taxId": "US-99182312"\n },\n "client": {\n "name": "Cyberdyne Systems"\n },\n "lineItems": [\n {\n "description": "GPU Cluster Instance H100",\n "quantity": 48,\n "unit": "hours",\n "unitPrice": 3.20,\n "totalPrice": 153.60\n },\n {\n "description": "NVMe High-Speed Storage 2TB",\n "quantity": 1,\n "unit": "month",\n "unitPrice": 80.00,\n "totalPrice": 80.00\n }\n ],\n "subtotal": 233.60,\n "taxRate": 0.08,\n "taxAmount": 18.69,\n "totalAmount": 252.29,\n "currency": "USD"\n}', mockTokens: 215, mockLatency: '0.6s', }, ] export function PromptPlayground({ className }: { className?: string }) { const [selectedModel, setSelectedModel] = React.useState('claude-3-5-sonnet') const [selectedPreset, setSelectedPreset] = React.useState('code-reviewer') const [systemPrompt, setSystemPrompt] = React.useState(presets[0].systemPrompt) const [userMessage, setUserMessage] = React.useState(presets[0].userMessage) const [temperature, setTemperature] = React.useState(presets[0].temperature) const [maxTokens, setMaxTokens] = React.useState(presets[0].maxTokens) const [topP, setTopP] = React.useState(presets[0].topP) const [jsonMode, setJsonMode] = React.useState(presets[0].jsonMode) const [streamResponses, setStreamResponses] = React.useState(presets[0].stream) const [displayedResponse, setDisplayedResponse] = React.useState(presets[0].mockResponse) const [responseStatus, setResponseStatus] = React.useState('200 OK · 1.4s · 428 tokens') const [isGenerating, setIsGenerating] = React.useState(false) const [showRawJson, setShowRawJson] = React.useState(false) const [isCopied, setIsCopied] = React.useState(false) const [isPresetSaved, setIsPresetSaved] = React.useState(false) const streamTimerRef = React.useRef<ReturnType<typeof setInterval> | null>(null) const activeModel = React.useMemo(() => { return models.find((m) => m.id === selectedModel) ?? models[0] }, [selectedModel]) const estimatedPromptTokens = React.useMemo(() => { const combined = (systemPrompt || '') + (userMessage || '') return Math.max(1, Math.round(combined.length / 3.8)) }, [systemPrompt, userMessage]) const estimatedTotalTokens = React.useMemo(() => { const outputEstimate = isGenerating ? Math.round(maxTokens / 4) : 428 return estimatedPromptTokens + outputEstimate }, [estimatedPromptTokens, isGenerating, maxTokens]) const estimatedCost = React.useMemo(() => { const inputCost = (estimatedPromptTokens / 1_000_000) * activeModel.inputCostPer1M const outputCost = (428 / 1_000_000) * activeModel.outputCostPer1M return (inputCost + outputCost).toFixed(4) }, [estimatedPromptTokens, activeModel]) const rawJsonPayload = React.useMemo(() => { return JSON.stringify( { id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`, object: 'chat.completion', created: 1787313600, model: selectedModel, system_fingerprint: 'fp_uipkge_49a', choices: [ { index: 0, message: { role: 'assistant', content: displayedResponse, }, finish_reason: 'stop', }, ], usage: { prompt_tokens: estimatedPromptTokens, completion_tokens: 428, total_tokens: estimatedPromptTokens + 428, }, configuration: { temperature, max_tokens: maxTokens, top_p: topP, response_format: jsonMode ? { type: 'json_object' } : { type: 'text' }, stream: streamResponses, }, }, null, 2, ) }, [selectedModel, displayedResponse, estimatedPromptTokens, temperature, maxTokens, topP, jsonMode, streamResponses]) const handlePresetChange = React.useCallback((presetId: string) => { setSelectedPreset(presetId) const preset = presets.find((p) => p.id === presetId) if (!preset) return setSystemPrompt(preset.systemPrompt) setUserMessage(preset.userMessage) setSelectedModel(preset.model) setTemperature(preset.temperature) setMaxTokens(preset.maxTokens) setTopP(preset.topP) setJsonMode(preset.jsonMode) setStreamResponses(preset.stream) setDisplayedResponse(preset.mockResponse) setResponseStatus(`200 OK · ${preset.mockLatency} · ${preset.mockTokens} tokens`) }, []) const runPrompt = React.useCallback(() => { if (isGenerating) return if (streamTimerRef.current) clearInterval(streamTimerRef.current) const activePresetObj = presets.find((p) => p.id === selectedPreset) ?? presets[0] const targetResponse = jsonMode && !activePresetObj.jsonMode ? presets[2].mockResponse : activePresetObj.mockResponse const targetLatency = activePresetObj.mockLatency const targetTokens = activePresetObj.mockTokens setIsGenerating(true) setResponseStatus('Generating...') if (streamResponses) { setDisplayedResponse('') let charIndex = 0 const chunkSize = 8 const totalChars = targetResponse.length streamTimerRef.current = setInterval(() => { charIndex += chunkSize if (charIndex >= totalChars) { setDisplayedResponse(targetResponse) if (streamTimerRef.current) clearInterval(streamTimerRef.current) streamTimerRef.current = null setIsGenerating(false) setResponseStatus(`200 OK · ${targetLatency} · ${targetTokens} tokens`) } else { setDisplayedResponse(targetResponse.slice(0, charIndex)) } }, 25) } else { setTimeout(() => { setDisplayedResponse(targetResponse) setIsGenerating(false) setResponseStatus(`200 OK · ${targetLatency} · ${targetTokens} tokens`) }, 600) } }, [isGenerating, selectedPreset, jsonMode, streamResponses]) const handleCopyResponse = React.useCallback(() => { if (!displayedResponse) return navigator.clipboard?.writeText(displayedResponse) setIsCopied(true) setTimeout(() => setIsCopied(false), 2000) }, [displayedResponse]) const handleSavePreset = React.useCallback(() => { setIsPresetSaved(true) setTimeout(() => setIsPresetSaved(false), 2000) }, []) const handleResetDefaults = React.useCallback(() => { setTemperature(0.7) setMaxTokens(2048) setTopP(0.9) setJsonMode(false) setStreamResponses(true) }, []) React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault() runPrompt() } } window.addEventListener('keydown', handleKeyDown) return () => { window.removeEventListener('keydown', handleKeyDown) if (streamTimerRef.current) clearInterval(streamTimerRef.current) } }, [runPrompt]) return ( <div data-slot="prompt-playground" className={cn('w-full space-y-6', className)}> {/* Top Toolbar */} <div className="border-border bg-card/70 flex flex-wrap items-center justify-between gap-3 rounded-lg border p-3.5 shadow-xs backdrop-blur-xs"> <div className="flex flex-wrap items-center gap-2.5"> {/* Model Selector */} <div className="w-48"> <Select value={selectedModel} onValueChange={setSelectedModel}> <SelectTrigger className="h-9 w-full text-xs font-medium" aria-label="Select AI Model"> <SelectValue placeholder="Select model" /> </SelectTrigger> <SelectContent> {models.map((m) => ( <SelectItem key={m.id} value={m.id}> <div className="flex items-center gap-2"> <Sparkles className="text-primary size-3.5" /> <span>{m.name}</span> </div> </SelectItem> ))} </SelectContent> </Select> </div> {/* Preset Template Dropdown */} <div className="w-52"> <Select value={selectedPreset} onValueChange={handlePresetChange}> <SelectTrigger className="h-9 w-full text-xs" aria-label="Select Preset Template"> <SelectValue placeholder="Preset template" /> </SelectTrigger> <SelectContent> {presets.map((p) => ( <SelectItem key={p.id} value={p.id}> <span>{p.name}</span> </SelectItem> ))} </SelectContent> </Select> </div> {/* Token Counter Pill */} <div className="border-border bg-muted/60 text-muted-foreground hidden items-center gap-1.5 rounded-full border px-3 py-1 font-mono text-xs md:inline-flex"> <Cpu className="text-primary size-3.5" /> <span>{estimatedTotalTokens.toLocaleString()} tokens</span> <span className="text-foreground font-medium">${estimatedCost}</span> </div> </div> <div className="flex items-center gap-2"> {/* Save Preset Outline Button */} <Button variant="outline" size="sm" className="h-9 gap-1.5 text-xs font-medium" onClick={handleSavePreset}> {isPresetSaved ? <Check className="text-success size-3.5" /> : <Save className="size-3.5" />} <span>{isPresetSaved ? 'Saved!' : 'Save Preset'}</span> </Button> {/* Run Prompt Primary Button */} <Button size="sm" className="bg-primary text-primary-foreground h-9 gap-1.5 text-xs font-medium shadow-xs" disabled={isGenerating} onClick={runPrompt} > {isGenerating ? ( <span className="border-primary-foreground size-3.5 animate-spin rounded-full border-2 border-t-transparent" /> ) : ( <Zap className="size-3.5 fill-current" /> )} <span>{isGenerating ? 'Running...' : 'Run Prompt'}</span> <kbd className="bg-primary-foreground/20 hidden items-center rounded px-1 py-0.5 font-mono text-xs sm:inline-flex"> ⌘↵ </kbd> </Button> </div> </div> {/* 2-Column Studio Layout */} <div className="grid grid-cols-1 gap-6 lg:grid-cols-12"> {/* Left Panel: Prompt Configuration & Test Input */} <div className="space-y-6 lg:col-span-7"> {/* System Instructions Card */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-wrap items-center justify-between"> <div className="flex items-center gap-2"> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <Bot className="size-4" /> </div> <div> <CardTitle className="text-sm font-semibold">System Instructions</CardTitle> <CardDescription className="text-xs"> Behavior, role personality, and operational constraints </CardDescription> </div> </div> <Badge variant="secondary" className="font-mono text-xs"> role: system </Badge> </div> </CardHeader> <CardContent className="pt-0"> <Textarea value={systemPrompt} onValueChange={setSystemPrompt} placeholder="You are an expert AI assistant..." rows={4} noResize className="border-border/70 focus:border-primary font-mono text-xs leading-relaxed" /> </CardContent> <CardFooter className="border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs"> <span className="flex flex-wrap items-center gap-1.5"> <FileCode className="size-3.5" /> <span>Context: {activeModel.contextWindow}</span> </span> <span className="font-mono">{systemPrompt.length} chars</span> </CardFooter> </Card> {/* User Message Input Card */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-wrap items-center justify-between"> <div className="flex items-center gap-2"> <div className="bg-secondary text-secondary-foreground flex size-7 items-center justify-center rounded-md"> <MessageSquare className="size-4" /> </div> <div> <CardTitle className="text-sm font-semibold">User Message</CardTitle> <CardDescription className="text-xs">Prompt input payload and test variables</CardDescription> </div> </div> <Badge variant="outline" className="font-mono text-xs"> role: user </Badge> </div> </CardHeader> <CardContent className="pt-0"> <Textarea value={userMessage} onValueChange={setUserMessage} placeholder="Enter user test prompt..." rows={7} noResize className="border-border/70 focus:border-primary font-mono text-xs leading-relaxed" /> </CardContent> <CardFooter className="border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs"> <div className="flex items-center gap-2"> <span className="text-muted-foreground text-xs">Quick inject:</span> <button type="button" className="hover:border-primary/60 hover:text-foreground min-h-6 rounded border border-dashed px-1.5 py-0.5 font-mono text-xs transition-colors" onClick={() => setUserMessage((prev) => prev + '\n\nEnsure strict backward compatibility.')} > +compatibility </button> <button type="button" className="hover:border-primary/60 hover:text-foreground min-h-6 rounded border border-dashed px-1.5 py-0.5 font-mono text-xs transition-colors" onClick={() => setUserMessage((prev) => prev + '\n\nOutput in concise bullet points.')} > +bullets </button> </div> <span className="font-mono">~{estimatedPromptTokens} tokens</span> </CardFooter> </Card> </div> {/* Right Panel: Parameters Sidebar + Output Card */} <div className="space-y-6 lg:col-span-5"> {/* Right Parameter Sidebar Card */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-wrap items-center justify-between"> <div className="flex items-center gap-2"> <div className="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <Sliders className="size-4" /> </div> <CardTitle className="text-sm font-semibold">Parameters</CardTitle> </div> <Button variant="ghost" size="sm" className="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" onClick={handleResetDefaults} > <RotateCcw className="mr-1 size-3" /> <span>Reset</span> </Button> </div> </CardHeader> <CardContent className="space-y-4 pt-0"> {/* Temperature Slider */} <div className="space-y-2"> <div className="flex flex-wrap items-center justify-between text-xs"> <label className="text-foreground font-medium">Temperature</label> <span className="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium"> {Number(temperature).toFixed(2)} </span> </div> <Slider value={[temperature]} onValueChange={(val) => setTemperature(val[0])} min={0} max={1} step={0.05} /> <div className="text-muted-foreground flex justify-between text-xs"> <span>0.0 (Deterministic)</span> <span>1.0 (Creative)</span> </div> </div> {/* Max Tokens Slider */} <div className="space-y-2"> <div className="flex flex-wrap items-center justify-between text-xs"> <label className="text-foreground font-medium">Max Tokens</label> <span className="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium"> {Number(maxTokens).toLocaleString()} </span> </div> <Slider value={[maxTokens]} onValueChange={(val) => setMaxTokens(val[0])} min={256} max={4096} step={128} /> <div className="text-muted-foreground flex justify-between text-xs"> <span>256</span> <span>4,096</span> </div> </div> {/* Top P Slider */} <div className="space-y-2"> <div className="flex flex-wrap items-center justify-between text-xs"> <label className="text-foreground font-medium">Top P</label> <span className="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium"> {Number(topP).toFixed(2)} </span> </div> <Slider value={[topP]} onValueChange={(val) => setTopP(val[0])} min={0} max={1} step={0.05} /> <div className="text-muted-foreground flex justify-between text-xs"> <span>0.0 (Focused)</span> <span>1.0 (Diverse)</span> </div> </div> <Separator /> {/* Switches */} <div className="space-y-3"> <div className="flex flex-wrap items-center justify-between gap-2"> <div className="space-y-0.5"> <label htmlFor="react-json-mode" className="text-foreground cursor-pointer text-xs font-medium"> JSON Mode </label> <p className="text-muted-foreground text-xs">Enforce structured JSON output</p> </div> <Switch id="react-json-mode" checked={jsonMode} onCheckedChange={setJsonMode} /> </div> <div className="flex flex-wrap items-center justify-between gap-2"> <div className="space-y-0.5"> <label htmlFor="react-stream-responses" className="text-foreground cursor-pointer text-xs font-medium" > Stream Responses </label> <p className="text-muted-foreground text-xs">Simulate real-time token streaming</p> </div> <Switch id="react-stream-responses" checked={streamResponses} onCheckedChange={setStreamResponses} /> </div> </div> </CardContent> </Card> {/* Response Output Card */} <Card className="border-border bg-card shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-wrap items-center justify-between"> <div className="flex items-center gap-2"> <div className="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <Terminal className="size-4" /> </div> <div> <CardTitle className="text-sm font-semibold">Response Output</CardTitle> </div> </div> <div className="flex flex-wrap items-center gap-1.5"> <Button variant="ghost" size="sm" className={cn( 'h-7 px-2 text-xs', showRawJson ? 'text-primary bg-primary/10' : 'text-muted-foreground', )} onClick={() => setShowRawJson(!showRawJson)} > <Braces className="mr-1 size-3.5" /> <span>{showRawJson ? 'Formatted' : 'Raw JSON'}</span> </Button> <Button variant="ghost" size="sm" className="text-muted-foreground hover:text-foreground h-7 px-2 text-xs" onClick={handleCopyResponse} > {isCopied ? <Check className="text-success mr-1 size-3.5" /> : <Copy className="mr-1 size-3.5" />} <span>{isCopied ? 'Copied' : 'Copy'}</span> </Button> </div> </div> <div className="mt-2 flex items-center gap-2"> <Badge variant="outline" className={cn( 'font-mono text-xs', isGenerating ? 'border-primary/40 text-primary animate-pulse' : 'border-success/30 text-success', )} > <span className={cn( 'mr-1.5 inline-block size-1.5 rounded-full', isGenerating ? 'bg-primary' : 'bg-success', )} /> {responseStatus} </Badge> </div> </CardHeader> <CardContent className="pt-0"> {/* Raw JSON View */} {showRawJson ? ( <div className="relative"> <pre className="border-border bg-muted/40 text-foreground max-h-[380px] overflow-x-auto overflow-y-auto rounded-md border p-3.5 font-mono text-xs leading-relaxed"> <code>{rawJsonPayload}</code> </pre> </div> ) : ( /* Prose / Markdown View */ <div className="border-border bg-muted/20 text-foreground max-h-[380px] min-h-[220px] overflow-y-auto rounded-md border p-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap"> {displayedResponse} {isGenerating && <span className="text-primary ml-0.5 inline-block animate-pulse">▋</span>} </div> )} </CardContent> <CardFooter className="border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs"> <span className="flex items-center gap-1"> <Clock className="size-3.5" /> <span>Finish: stop</span> </span> <span className="font-mono">Throughput: ~305 tok/s</span> </CardFooter> </Card> </div> </div> </div> ) }
Raw manifest:https://uipkge.dev/r/react/prompt-playground.json