{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "prompt-playground",
  "title": "Prompt Playground",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/prompt-playground/prompt-playground.component.ts",
      "content": "// Edit this file to change the models, the presets (prompts + mock responses) or the copy.\n// Responses are mocked: \"Run Prompt\" (or Cmd/Ctrl+Enter) replays the preset's response,\n// streamed in 8-character chunks when \"Stream Responses\" is on.\nimport { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  Bot,\n  Braces,\n  Check,\n  Clock,\n  Copy,\n  Cpu,\n  FileCode,\n  LucideAngularModule,\n  MessageSquare,\n  RotateCcw,\n  Save,\n  Sliders,\n  Sparkles,\n  Terminal,\n  Zap,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardFooterComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSliderComponent } from '@/ui/slider/slider.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\nimport { UiTextareaComponent } from '@/ui/textarea/textarea.component'\n\nexport interface PromptPlaygroundModel {\n  id: string\n  name: string\n  provider: string\n  contextWindow: string\n  inputCostPer1M: number\n  outputCostPer1M: number\n}\n\nexport interface PromptPlaygroundPreset {\n  id: string\n  name: string\n  description: string\n  model: string\n  temperature: number\n  maxTokens: number\n  topP: number\n  jsonMode: boolean\n  stream: boolean\n  systemPrompt: string\n  userMessage: string\n  mockResponse: string\n  mockTokens: number\n  mockLatency: string\n}\n\nconst models: PromptPlaygroundModel[] = [\n  {\n    id: 'claude-3-5-sonnet',\n    name: 'Claude 3.5 Sonnet',\n    provider: 'Anthropic',\n    contextWindow: '200k',\n    inputCostPer1M: 3.0,\n    outputCostPer1M: 15.0,\n  },\n  {\n    id: 'gpt-4o',\n    name: 'GPT-4o',\n    provider: 'OpenAI',\n    contextWindow: '128k',\n    inputCostPer1M: 2.5,\n    outputCostPer1M: 10.0,\n  },\n  {\n    id: 'llama-3-3-70b',\n    name: 'Llama 3.3 70B',\n    provider: 'Meta',\n    contextWindow: '128k',\n    inputCostPer1M: 0.6,\n    outputCostPer1M: 0.8,\n  },\n]\n\nconst presets: PromptPlaygroundPreset[] = [\n  {\n    id: 'code-reviewer',\n    name: 'Code Reviewer',\n    description: 'Security & vulnerability audit for backend code',\n    model: 'claude-3-5-sonnet',\n    temperature: 0.2,\n    maxTokens: 2048,\n    topP: 0.9,\n    jsonMode: false,\n    stream: true,\n    systemPrompt:\n      '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.',\n    userMessage:\n      '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```',\n    mockResponse:\n      '### 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.',\n    mockTokens: 428,\n    mockLatency: '1.4s',\n  },\n  {\n    id: 'customer-support',\n    name: 'Customer Support Agent',\n    description: 'Empathetic, brand-aligned troubleshooting for customer inquiries',\n    model: 'gpt-4o',\n    temperature: 0.7,\n    maxTokens: 1024,\n    topP: 0.95,\n    jsonMode: false,\n    stream: true,\n    systemPrompt:\n      '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.',\n    userMessage:\n      '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?\"',\n    mockResponse:\n      '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**',\n    mockTokens: 286,\n    mockLatency: '0.9s',\n  },\n  {\n    id: 'json-extractor',\n    name: 'JSON Extractor',\n    description: 'Strict schema extraction from unstructured invoices or logs',\n    model: 'gpt-4o',\n    temperature: 0.0,\n    maxTokens: 1024,\n    topP: 0.1,\n    jsonMode: true,\n    stream: false,\n    systemPrompt:\n      '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.',\n    userMessage:\n      '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',\n    mockResponse:\n      '{\\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}',\n    mockTokens: 215,\n    mockLatency: '0.6s',\n  },\n]\n\nconst defaultPreset = presets[0]!\n\n/**\n * PromptPlayground -- the React block, 1:1: an AI prompt studio with a toolbar (model +\n * preset selects, token / cost pill, Save Preset, Run Prompt), system and user message\n * cards, a parameters card (temperature, max tokens, top-p sliders, JSON / stream\n * switches) and a response card with a formatted / raw JSON toggle and copy.\n *\n * Icons inside buttons sit in <lucide-icon>, so the button's `has-[>svg]:px-2.5` padding\n * does not match; `px-2.5` is written out where React gets it from that variant.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-prompt-playground, [ui-prompt-playground]',\n  standalone: true,\n  // React renders the root <div> itself: keep this wrapper out of the layout.\n  host: { class: 'contents', '(window:keydown)': 'onWindowKeydown($event)' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardFooterComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiSliderComponent,\n    UiSwitchComponent,\n    UiTextareaComponent,\n  ],\n  template: `\n    <div data-slot=\"prompt-playground\" [class]=\"rootClass\">\n      <!-- Top Toolbar -->\n      <div\n        class=\"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\"\n      >\n        <div class=\"flex flex-wrap items-center gap-2.5\">\n          <!-- Model Selector -->\n          <div class=\"w-48\">\n            <ui-select [value]=\"selectedModel()\" (valueChange)=\"selectedModel.set($event)\">\n              <button ui-select-trigger class=\"h-9 w-full text-xs font-medium\" aria-label=\"Select AI Model\">\n                <ui-select-value placeholder=\"Select model\" />\n              </button>\n              <ui-select-content>\n                @for (m of models; track m.id) {\n                  <ui-select-item [value]=\"m.id\">\n                    <div class=\"flex items-center gap-2\">\n                      <lucide-icon [img]=\"Sparkles\" class=\"text-primary size-3.5\" />\n                      <span>{{ m.name }}</span>\n                    </div>\n                  </ui-select-item>\n                }\n              </ui-select-content>\n            </ui-select>\n          </div>\n\n          <!-- Preset Template Dropdown -->\n          <div class=\"w-52\">\n            <ui-select [value]=\"selectedPreset()\" (valueChange)=\"handlePresetChange($event)\">\n              <button ui-select-trigger class=\"h-9 w-full text-xs\" aria-label=\"Select Preset Template\">\n                <ui-select-value placeholder=\"Preset template\" />\n              </button>\n              <ui-select-content>\n                @for (p of presets; track p.id) {\n                  <ui-select-item [value]=\"p.id\">\n                    <span>{{ p.name }}</span>\n                  </ui-select-item>\n                }\n              </ui-select-content>\n            </ui-select>\n          </div>\n\n          <!-- Token Counter Pill -->\n          <div\n            class=\"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\"\n          >\n            <lucide-icon [img]=\"Cpu\" class=\"text-primary size-3.5\" />\n            <span>{{ estimatedTotalTokens().toLocaleString() }} tokens</span>\n            <span class=\"text-foreground font-medium\">\\${{ estimatedCost() }}</span>\n          </div>\n        </div>\n\n        <div class=\"flex items-center gap-2\">\n          <!-- Save Preset Outline Button -->\n          <button\n            ui-button\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"h-9 gap-1.5 px-2.5 text-xs font-medium\"\n            (click)=\"handleSavePreset()\"\n          >\n            @if (isPresetSaved()) {\n              <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n            } @else {\n              <lucide-icon [img]=\"Save\" class=\"size-3.5\" />\n            }\n            <span>{{ isPresetSaved() ? 'Saved!' : 'Save Preset' }}</span>\n          </button>\n\n          <!-- Run Prompt Primary Button -->\n          <button ui-button size=\"sm\" [class]=\"runButtonClass()\" [disabled]=\"isGenerating()\" (click)=\"runPrompt()\">\n            @if (isGenerating()) {\n              <span\n                class=\"border-primary-foreground size-3.5 animate-spin rounded-full border-2 border-t-transparent\"\n              ></span>\n            } @else {\n              <lucide-icon [img]=\"Zap\" class=\"size-3.5 fill-current\" />\n            }\n            <span>{{ isGenerating() ? 'Running...' : 'Run Prompt' }}</span>\n            <kbd\n              class=\"bg-primary-foreground/20 hidden items-center rounded px-1 py-0.5 font-mono text-xs sm:inline-flex\"\n              >⌘↵</kbd\n            >\n          </button>\n        </div>\n      </div>\n\n      <!-- 2-Column Studio Layout -->\n      <div class=\"grid grid-cols-1 gap-6 lg:grid-cols-12\">\n        <!-- Left Panel: Prompt Configuration & Test Input -->\n        <div class=\"space-y-6 lg:col-span-7\">\n          <!-- System Instructions Card -->\n          <ui-card class=\"border-border bg-card shadow-xs\">\n            <ui-card-header class=\"pb-3\">\n              <div class=\"flex flex-wrap items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <div class=\"bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md\">\n                    <lucide-icon [img]=\"Bot\" class=\"size-4\" />\n                  </div>\n                  <div>\n                    <ui-card-title class=\"text-sm font-semibold\">System Instructions</ui-card-title>\n                    <ui-card-description class=\"text-xs\"\n                      >Behavior, role personality, and operational constraints</ui-card-description\n                    >\n                  </div>\n                </div>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">role: system</span>\n              </div>\n            </ui-card-header>\n            <ui-card-content class=\"pt-0\">\n              <ui-textarea\n                [value]=\"systemPrompt()\"\n                (valueChange)=\"systemPrompt.set($event)\"\n                placeholder=\"You are an expert AI assistant...\"\n                [rows]=\"4\"\n                noResize\n                class=\"border-border/70 focus:border-primary font-mono text-xs leading-relaxed\"\n              />\n            </ui-card-content>\n            <ui-card-footer\n              class=\"border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs\"\n            >\n              <span class=\"flex flex-wrap items-center gap-1.5\">\n                <lucide-icon [img]=\"FileCode\" class=\"size-3.5\" />\n                <span>Context: {{ activeModel().contextWindow }}</span>\n              </span>\n              <span class=\"font-mono\">{{ systemPrompt().length }} chars</span>\n            </ui-card-footer>\n          </ui-card>\n\n          <!-- User Message Input Card -->\n          <ui-card class=\"border-border bg-card shadow-xs\">\n            <ui-card-header class=\"pb-3\">\n              <div class=\"flex flex-wrap items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <div\n                    class=\"bg-secondary text-secondary-foreground flex size-7 items-center justify-center rounded-md\"\n                  >\n                    <lucide-icon [img]=\"MessageSquare\" class=\"size-4\" />\n                  </div>\n                  <div>\n                    <ui-card-title class=\"text-sm font-semibold\">User Message</ui-card-title>\n                    <ui-card-description class=\"text-xs\">Prompt input payload and test variables</ui-card-description>\n                  </div>\n                </div>\n                <span ui-badge variant=\"outline\" class=\"font-mono text-xs\">role: user</span>\n              </div>\n            </ui-card-header>\n            <ui-card-content class=\"pt-0\">\n              <ui-textarea\n                [value]=\"userMessage()\"\n                (valueChange)=\"userMessage.set($event)\"\n                placeholder=\"Enter user test prompt...\"\n                [rows]=\"7\"\n                noResize\n                class=\"border-border/70 focus:border-primary font-mono text-xs leading-relaxed\"\n              />\n            </ui-card-content>\n            <ui-card-footer\n              class=\"border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs\"\n            >\n              <div class=\"flex items-center gap-2\">\n                <span class=\"text-muted-foreground text-xs\">Quick inject:</span>\n                <button\n                  type=\"button\"\n                  class=\"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\"\n                  (click)=\"quickInject('\\\\n\\\\nEnsure strict backward compatibility.')\"\n                >\n                  +compatibility\n                </button>\n                <button\n                  type=\"button\"\n                  class=\"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\"\n                  (click)=\"quickInject('\\\\n\\\\nOutput in concise bullet points.')\"\n                >\n                  +bullets\n                </button>\n              </div>\n              <span class=\"font-mono\">~{{ estimatedPromptTokens() }} tokens</span>\n            </ui-card-footer>\n          </ui-card>\n        </div>\n\n        <!-- Right Panel: Parameters Sidebar + Output Card -->\n        <div class=\"space-y-6 lg:col-span-5\">\n          <!-- Right Parameter Sidebar Card -->\n          <ui-card class=\"border-border bg-card shadow-xs\">\n            <ui-card-header class=\"pb-3\">\n              <div class=\"flex flex-wrap items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <div class=\"bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md\">\n                    <lucide-icon [img]=\"Sliders\" class=\"size-4\" />\n                  </div>\n                  <ui-card-title class=\"text-sm font-semibold\">Parameters</ui-card-title>\n                </div>\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground h-7 px-2.5 text-xs\"\n                  (click)=\"handleResetDefaults()\"\n                >\n                  <lucide-icon [img]=\"RotateCcw\" [class]=\"'mr-1 size-3'\" />\n                  <span>Reset</span>\n                </button>\n              </div>\n            </ui-card-header>\n            <ui-card-content class=\"space-y-4 pt-0\">\n              <!-- Temperature Slider -->\n              <div class=\"space-y-2\">\n                <div class=\"flex flex-wrap items-center justify-between text-xs\">\n                  <label class=\"text-foreground font-medium\">Temperature</label>\n                  <span class=\"bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium\">{{\n                    temperature().toFixed(2)\n                  }}</span>\n                </div>\n                <ui-slider\n                  [value]=\"[temperature()]\"\n                  (valueChange)=\"temperature.set(first($event))\"\n                  [min]=\"0\"\n                  [max]=\"1\"\n                  [step]=\"0.05\"\n                />\n                <div class=\"text-muted-foreground flex justify-between text-xs\">\n                  <span>0.0 (Deterministic)</span>\n                  <span>1.0 (Creative)</span>\n                </div>\n              </div>\n\n              <!-- Max Tokens Slider -->\n              <div class=\"space-y-2\">\n                <div class=\"flex flex-wrap items-center justify-between text-xs\">\n                  <label class=\"text-foreground font-medium\">Max Tokens</label>\n                  <span class=\"bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium\">{{\n                    maxTokens().toLocaleString()\n                  }}</span>\n                </div>\n                <ui-slider\n                  [value]=\"[maxTokens()]\"\n                  (valueChange)=\"maxTokens.set(first($event))\"\n                  [min]=\"256\"\n                  [max]=\"4096\"\n                  [step]=\"128\"\n                />\n                <div class=\"text-muted-foreground flex justify-between text-xs\">\n                  <span>256</span>\n                  <span>4,096</span>\n                </div>\n              </div>\n\n              <!-- Top P Slider -->\n              <div class=\"space-y-2\">\n                <div class=\"flex flex-wrap items-center justify-between text-xs\">\n                  <label class=\"text-foreground font-medium\">Top P</label>\n                  <span class=\"bg-muted text-foreground rounded px-1.5 py-0.5 font-mono font-medium\">{{\n                    topP().toFixed(2)\n                  }}</span>\n                </div>\n                <ui-slider\n                  [value]=\"[topP()]\"\n                  (valueChange)=\"topP.set(first($event))\"\n                  [min]=\"0\"\n                  [max]=\"1\"\n                  [step]=\"0.05\"\n                />\n                <div class=\"text-muted-foreground flex justify-between text-xs\">\n                  <span>0.0 (Focused)</span>\n                  <span>1.0 (Diverse)</span>\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <!-- Switches -->\n              <div class=\"space-y-3\">\n                <div class=\"flex flex-wrap items-center justify-between gap-2\">\n                  <div class=\"space-y-0.5\">\n                    <label for=\"angular-json-mode\" class=\"text-foreground cursor-pointer text-xs font-medium\">\n                      JSON Mode\n                    </label>\n                    <p class=\"text-muted-foreground text-xs\">Enforce structured JSON output</p>\n                  </div>\n                  <button\n                    ui-switch\n                    id=\"angular-json-mode\"\n                    [checked]=\"jsonMode()\"\n                    (checkedChange)=\"jsonMode.set($event)\"\n                  ></button>\n                </div>\n\n                <div class=\"flex flex-wrap items-center justify-between gap-2\">\n                  <div class=\"space-y-0.5\">\n                    <label for=\"angular-stream-responses\" class=\"text-foreground cursor-pointer text-xs font-medium\">\n                      Stream Responses\n                    </label>\n                    <p class=\"text-muted-foreground text-xs\">Simulate real-time token streaming</p>\n                  </div>\n                  <button\n                    ui-switch\n                    id=\"angular-stream-responses\"\n                    [checked]=\"streamResponses()\"\n                    (checkedChange)=\"streamResponses.set($event)\"\n                  ></button>\n                </div>\n              </div>\n            </ui-card-content>\n          </ui-card>\n\n          <!-- Response Output Card -->\n          <ui-card class=\"border-border bg-card shadow-xs\">\n            <ui-card-header class=\"pb-3\">\n              <div class=\"flex flex-wrap items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <div class=\"bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md\">\n                    <lucide-icon [img]=\"Terminal\" class=\"size-4\" />\n                  </div>\n                  <div>\n                    <ui-card-title class=\"text-sm font-semibold\">Response Output</ui-card-title>\n                  </div>\n                </div>\n                <div class=\"flex flex-wrap items-center gap-1.5\">\n                  <button ui-button variant=\"ghost\" size=\"sm\" [class]=\"rawToggleClass()\" (click)=\"toggleRawJson()\">\n                    <lucide-icon [img]=\"Braces\" [class]=\"'mr-1 size-3.5'\" />\n                    <span>{{ showRawJson() ? 'Formatted' : 'Raw JSON' }}</span>\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    class=\"text-muted-foreground hover:text-foreground h-7 px-2.5 text-xs\"\n                    (click)=\"handleCopyResponse()\"\n                  >\n                    @if (isCopied()) {\n                      <lucide-icon [img]=\"Check\" [class]=\"'text-success mr-1 size-3.5'\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" [class]=\"'mr-1 size-3.5'\" />\n                    }\n                    <span>{{ isCopied() ? 'Copied' : 'Copy' }}</span>\n                  </button>\n                </div>\n              </div>\n              <div class=\"mt-2 flex items-center gap-2\">\n                <span ui-badge variant=\"outline\" [class]=\"statusBadgeClass()\">\n                  <span [class]=\"statusDotClass()\"></span>{{ responseStatus() }}\n                </span>\n              </div>\n            </ui-card-header>\n            <ui-card-content class=\"pt-0\">\n              @if (showRawJson()) {\n                <!-- Raw JSON View -->\n                <div class=\"relative\">\n                  <pre\n                    class=\"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\"\n                  ><code>{{ rawJsonPayload() }}</code></pre>\n                </div>\n              } @else {\n                <!-- Prose / Markdown View: keep the interpolation tight (React renders response + cursor with no whitespace). -->\n                <div\n                  class=\"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\"\n                >{{ displayedResponse() }}@if (isGenerating()) {\n                    <span class=\"text-primary ml-0.5 inline-block animate-pulse\">▋</span>\n                  }</div>\n              }\n            </ui-card-content>\n            <ui-card-footer\n              class=\"border-border text-muted-foreground flex flex-wrap items-center justify-between border-t pt-3 text-xs\"\n            >\n              <span class=\"flex items-center gap-1\">\n                <lucide-icon [img]=\"Clock\" class=\"size-3.5\" />\n                <span>Finish: stop</span>\n              </span>\n              <span class=\"font-mono\">Throughput: ~305 tok/s</span>\n            </ui-card-footer>\n          </ui-card>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiPromptPlaygroundComponent {\n  /** Extra classes for the root element (React `className`). */\n  @Input('class') className?: string\n\n  readonly Bot = Bot\n  readonly Braces = Braces\n  readonly Check = Check\n  readonly Clock = Clock\n  readonly Copy = Copy\n  readonly Cpu = Cpu\n  readonly FileCode = FileCode\n  readonly MessageSquare = MessageSquare\n  readonly RotateCcw = RotateCcw\n  readonly Save = Save\n  readonly Sliders = Sliders\n  readonly Sparkles = Sparkles\n  readonly Terminal = Terminal\n  readonly Zap = Zap\n\n  readonly models = models\n  readonly presets = presets\n\n  readonly selectedModel = signal('claude-3-5-sonnet')\n  readonly selectedPreset = signal('code-reviewer')\n\n  readonly systemPrompt = signal(defaultPreset.systemPrompt)\n  readonly userMessage = signal(defaultPreset.userMessage)\n  readonly temperature = signal(defaultPreset.temperature)\n  readonly maxTokens = signal(defaultPreset.maxTokens)\n  readonly topP = signal(defaultPreset.topP)\n  readonly jsonMode = signal(defaultPreset.jsonMode)\n  readonly streamResponses = signal(defaultPreset.stream)\n\n  readonly displayedResponse = signal(defaultPreset.mockResponse)\n  readonly responseStatus = signal('200 OK · 1.4s · 428 tokens')\n  readonly isGenerating = signal(false)\n  readonly showRawJson = signal(false)\n  readonly isCopied = signal(false)\n  readonly isPresetSaved = signal(false)\n\n  private streamTimer: ReturnType<typeof setInterval> | null = null\n  private readonly timeouts = new Set<ReturnType<typeof setTimeout>>()\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      if (this.streamTimer) clearInterval(this.streamTimer)\n      for (const t of this.timeouts) clearTimeout(t)\n    })\n  }\n\n  readonly activeModel = computed(() => models.find((m) => m.id === this.selectedModel()) ?? models[0]!)\n\n  readonly estimatedPromptTokens = computed(() => {\n    const combined = (this.systemPrompt() || '') + (this.userMessage() || '')\n    return Math.max(1, Math.round(combined.length / 3.8))\n  })\n\n  readonly estimatedTotalTokens = computed(() => {\n    const outputEstimate = this.isGenerating() ? Math.round(this.maxTokens() / 4) : 428\n    return this.estimatedPromptTokens() + outputEstimate\n  })\n\n  readonly estimatedCost = computed(() => {\n    const model = this.activeModel()\n    const inputCost = (this.estimatedPromptTokens() / 1_000_000) * model.inputCostPer1M\n    const outputCost = (428 / 1_000_000) * model.outputCostPer1M\n    return (inputCost + outputCost).toFixed(4)\n  })\n\n  readonly rawJsonPayload = computed(() =>\n    JSON.stringify(\n      {\n        id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,\n        object: 'chat.completion',\n        created: 1787313600,\n        model: this.selectedModel(),\n        system_fingerprint: 'fp_uipkge_49a',\n        choices: [\n          {\n            index: 0,\n            message: {\n              role: 'assistant',\n              content: this.displayedResponse(),\n            },\n            finish_reason: 'stop',\n          },\n        ],\n        usage: {\n          prompt_tokens: this.estimatedPromptTokens(),\n          completion_tokens: 428,\n          total_tokens: this.estimatedPromptTokens() + 428,\n        },\n        configuration: {\n          temperature: this.temperature(),\n          max_tokens: this.maxTokens(),\n          top_p: this.topP(),\n          response_format: this.jsonMode() ? { type: 'json_object' } : { type: 'text' },\n          stream: this.streamResponses(),\n        },\n      },\n      null,\n      2,\n    ),\n  )\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  // px-2.5 idle: React gets it from size=\"sm\" has-[>svg] (the Zap icon); the spinner is a span (px-3).\n  readonly runButtonClass = computed(() =>\n    cn(\n      'bg-primary text-primary-foreground h-9 gap-1.5 text-xs font-medium shadow-xs',\n      !this.isGenerating() && 'px-2.5',\n    ),\n  )\n\n  readonly rawToggleClass = computed(() =>\n    cn('h-7 px-2.5 text-xs', this.showRawJson() ? 'text-primary bg-primary/10' : 'text-muted-foreground'),\n  )\n\n  readonly statusBadgeClass = computed(() =>\n    cn(\n      'font-mono text-xs',\n      this.isGenerating() ? 'border-primary/40 text-primary animate-pulse' : 'border-success/30 text-success',\n    ),\n  )\n\n  readonly statusDotClass = computed(() =>\n    cn('mr-1.5 inline-block size-1.5 rounded-full', this.isGenerating() ? 'bg-primary' : 'bg-success'),\n  )\n\n  /** Slider `valueChange` always emits number[]; one thumb here. */\n  first(values: number[]): number {\n    return values[0] ?? 0\n  }\n\n  private later(fn: () => void, ms: number): void {\n    const t = setTimeout(() => {\n      this.timeouts.delete(t)\n      fn()\n    }, ms)\n    this.timeouts.add(t)\n  }\n\n  handlePresetChange(presetId: string): void {\n    this.selectedPreset.set(presetId)\n    const preset = presets.find((p) => p.id === presetId)\n    if (!preset) return\n\n    this.systemPrompt.set(preset.systemPrompt)\n    this.userMessage.set(preset.userMessage)\n    this.selectedModel.set(preset.model)\n    this.temperature.set(preset.temperature)\n    this.maxTokens.set(preset.maxTokens)\n    this.topP.set(preset.topP)\n    this.jsonMode.set(preset.jsonMode)\n    this.streamResponses.set(preset.stream)\n    this.displayedResponse.set(preset.mockResponse)\n    this.responseStatus.set(`200 OK · ${preset.mockLatency} · ${preset.mockTokens} tokens`)\n  }\n\n  runPrompt(): void {\n    if (this.isGenerating()) return\n    if (this.streamTimer) clearInterval(this.streamTimer)\n\n    const activePreset = presets.find((p) => p.id === this.selectedPreset()) ?? defaultPreset\n    const targetResponse =\n      this.jsonMode() && !activePreset.jsonMode ? presets[2]!.mockResponse : activePreset.mockResponse\n    const doneStatus = `200 OK · ${activePreset.mockLatency} · ${activePreset.mockTokens} tokens`\n\n    this.isGenerating.set(true)\n    this.responseStatus.set('Generating...')\n\n    if (this.streamResponses()) {\n      this.displayedResponse.set('')\n      let charIndex = 0\n      const chunkSize = 8\n      const totalChars = targetResponse.length\n\n      this.streamTimer = setInterval(() => {\n        charIndex += chunkSize\n        if (charIndex >= totalChars) {\n          this.displayedResponse.set(targetResponse)\n          if (this.streamTimer) clearInterval(this.streamTimer)\n          this.streamTimer = null\n          this.isGenerating.set(false)\n          this.responseStatus.set(doneStatus)\n        } else {\n          this.displayedResponse.set(targetResponse.slice(0, charIndex))\n        }\n      }, 25)\n    } else {\n      this.later(() => {\n        this.displayedResponse.set(targetResponse)\n        this.isGenerating.set(false)\n        this.responseStatus.set(doneStatus)\n      }, 600)\n    }\n  }\n\n  quickInject(text: string): void {\n    this.userMessage.update((prev) => prev + text)\n  }\n\n  toggleRawJson(): void {\n    this.showRawJson.update((v) => !v)\n  }\n\n  handleCopyResponse(): void {\n    const text = this.displayedResponse()\n    if (!text) return\n    void navigator.clipboard?.writeText(text)\n    this.isCopied.set(true)\n    this.later(() => this.isCopied.set(false), 2000)\n  }\n\n  handleSavePreset(): void {\n    this.isPresetSaved.set(true)\n    this.later(() => this.isPresetSaved.set(false), 2000)\n  }\n\n  handleResetDefaults(): void {\n    this.temperature.set(0.7)\n    this.maxTokens.set(2048)\n    this.topP.set(0.9)\n    this.jsonMode.set(false)\n    this.streamResponses.set(true)\n  }\n\n  onWindowKeydown(e: KeyboardEvent): void {\n    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {\n      e.preventDefault()\n      this.runPrompt()\n    }\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/prompt-playground/prompt-playground.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/prompt-playground/index.ts",
      "content": "export {\n  UiPromptPlaygroundComponent,\n  type PromptPlaygroundModel,\n  type PromptPlaygroundPreset,\n} from './prompt-playground.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/prompt-playground/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/slider.json",
    "https://uipkge.dev/r/angular/switch.json",
    "https://uipkge.dev/r/angular/textarea.json"
  ],
  "description": "AI model prompt engineering studio & parameter tuner with system instructions, user input, temperature, max tokens, top-p sliders, JSON/stream modes, and interactive response evaluation.",
  "categories": [
    "ai",
    "app"
  ]
}