Browse documentation

Frontend Specs

RankFlow AI — Admin Dashboard Frontend Specification (Part 2)

Document Version: 1.0.0

docs/specs/frontend/admin-spec-02-ai-blog-prompts.md
On this page

Document Version: 1.0.0 Date: 2026-06-13 Owner: Frontend Engineering Scope: Sections 4–6 — AI Evaluation, Blog Manager, Prompt Manager Format: Markdown (.md) Tech Stack: Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS, tRPC, React Query, Zod, Better Auth, Recharts, date-fns, Lucide React Source Documents: backend-api.md, business_flow_map.md, ai-services.md, job-queue-workflows.md, frontend-spec-01-design-system.md, admin-spec-01-full.md


Design Token Compliance (Inherited)#

All UI components must adhere to the RankFlow design token system. Critical constraints:

Constraint Rule
Typography weight No bold. Maximum font-weight: 500 (Medium). Suisse Intl Medium only.
Ember Orange #EC652B reserved exclusively for CTAs, primary action buttons, focus rings, positive outcome indicators.
Page max-width 1200px centered. Canvas: Paper White #F6F6F8.
Sidebar 240px wide, carbon (#12161E) background for admin visual distinction.
Card radius 8px (border-radius-lg).
Medical compliance All content for medical clients (CLINIC/HOSPITAL/DOCTOR) goes through 24h approval queue.

Section 4 — AI Evaluation (/admin/ai-evaluation)#

Role access: ADMIN only. Non-ADMIN redirected to /dashboard with 403. Layout: KPI cards (top) + evaluation table (middle) + model performance charts (bottom). Sidebar: carbon (#12161E). Purpose: Monitor AI content quality, model performance, prompt effectiveness, and cost efficiency. The platform's core moat is AI-driven automation — this screen ensures the AI is performing optimally.


Widget 4.1 — AI Quality KPI Cards#

1. Screen Name & Route: /admin/ai-evaluation — AI Quality KPI Cards (top grid). Role: ADMIN only.

2. Data to Show:

KPI Type Format Calculation Data Source
Content Pieces Generated (24h) number Integer count(content.createdAt within 24h) admin.getAIEvaluation
Avg Generation Time duration Seconds, 1 decimal avg(content.generationDuration) admin.getAIEvaluation
Human Approval Rate percentage %, 1 decimal approved / total * 100 admin.getAIEvaluation
Rejection Rate percentage %, 1 decimal rejected / total * 100 admin.getAIEvaluation
AI Cost Per Piece currency USD, 3 decimals totalAIcost / piecesGenerated admin.getAIEvaluation
Avg Quality Score number 0–100, integer avg(content.qualityScore) admin.getAIEvaluation
Hallucination Rate percentage %, 2 decimals flaggedHallucinations / total * 100 admin.getAIEvaluation
Model Distribution chart Pie chart count by model (Claude Sonnet, Claude Haiku, GPT-4o, GPT-4o-mini) admin.getAIEvaluation
Prompt Effectiveness percentage %, 1 decimal avg(promptEffectivenessScore) admin.getAIEvaluation
Medical Content Accuracy percentage %, 1 decimal verifiedMedicalClaims / totalMedicalClaims * 100 admin.getAIEvaluation

3. Display Pattern:

  • Component: MetricCard (same as admin KPI cards, dense layout).
  • Layout: CSS Grid, 5 columns (desktop), 3 (tablet), 2 (mobile). Gap: space-4 (16px).
  • Container: card-white, radius-lg, shadow-subtle, padding 20px.
  • Header: Label (caption, 12px, text-tertiary, uppercase).
  • Body: Value (heading, 24px, weight 500).
  • Footer: Trend text (e.g., "+12% vs. yesterday") or sparkline.
  • Threshold badges: Quality Score < 70 → warning. Hallucination Rate > 2% → critical. Rejection Rate > 15% → alert.

4. Backend Endpoint:

  • Primary: admin.getAIEvaluation (tRPC query, adminProcedure).
  • Zod input schema: z.object({ period: z.enum(["24h", "7d", "30d"]).default("24h") }).
  • Zod output schema:
    z.object({
      piecesGenerated24h: z.number().int(),
      avgGenerationTime: z.number(), // seconds
      humanApprovalRate: z.number(),
      rejectionRate: z.number(),
      aiCostPerPiece: z.number(), // USD
      avgQualityScore: z.number().min(0).max(100),
      hallucinationRate: z.number(),
      modelDistribution: z.array(z.object({ model: z.string(), count: z.number(), cost: z.number() })),
      promptEffectiveness: z.number(),
      medicalContentAccuracy: z.number(),
      trendData: z.object({
        piecesGenerated: z.array(z.object({ date: z.string(), value: z.number() })),
        qualityScore: z.array(z.object({ date: z.string(), value: z.number() })),
        costPerPiece: z.array(z.object({ date: z.string(), value: z.number() })),
      }),
    })
    
  • Auth: adminProcedure. Returns 403 for non-admin.

5. Fetch Frequency:

  • Initial load: On /admin/ai-evaluation mount.
  • Polling: Every 60 seconds (refetchInterval: 60000).
  • Period change: Immediate refetch.

6. Data Manipulations:

  • avgGenerationTime: (totalGenerationTime / piecesGenerated).toFixed(1) seconds.
  • humanApprovalRate: (approvedCount / totalCount * 100).toFixed(1).
  • rejectionRate: (rejectedCount / totalCount * 100).toFixed(1).
  • aiCostPerPiece: (totalCost / piecesGenerated).toFixed(3) USD.
  • hallucinationRate: (hallucinationCount / totalCount * 100).toFixed(2).
  • medicalContentAccuracy: (verifiedMedical / totalMedical * 100).toFixed(1).
  • Thresholds: Quality Score < 70 → ember-orange warning. Hallucination > 2% → error-red critical. Rejection > 15% → error-red alert.
  • Trend formatting: +X% vs. previous period. Color: positive = forest-teal, negative = error-red.

7. Why Structured This Way:

  • 60-second polling: AI metrics change as content is generated. Approval/rejection rates update in real-time as admins review content.
  • 5-column grid: 10 KPIs in dense grid. Admin gets full AI health snapshot at a glance.
  • Hallucination rate: Critical metric for medical content. Even 1% hallucination is dangerous. Shown prominently with critical threshold.
  • Medical content accuracy: Separate from general quality score. Medical claims require higher scrutiny.
  • Cost per piece: Margin analysis. If cost per piece rises, admin needs to optimize prompts or switch models.
  • Model distribution: Helps optimize costs. Claude Haiku is cheaper than Sonnet. If Haiku can handle 80% of tasks, admin should shift usage.

8. Interaction Flows:

  • Hover: Card lifts, click navigates to filtered evaluation table for that metric.
  • Period selector: Dropdown above grid ("24h", "7d", "30d"). Change triggers refetch.
  • Loading: Skeleton cards (10 cards, pulsing).
  • Error: Inline error per card. Other cards unaffected.

9. Error States:

State Trigger UI Treatment Recovery
Empty No AI content generated yet "No AI generation data yet. Content will appear as clients use the platform." N/A
Loading Initial load Skeleton cards Data arrives
Error admin.getAIEvaluation fails Inline error per card with retry Retry
Partial Some metrics missing Show available metrics, "—" for missing N/A

10. Role-Based Variations:

  • ADMIN: Full access, all 10 cards, all thresholds, period selector.
  • Other roles: No access. Redirected to /dashboard.

Widget 4.2 — Content Evaluation Table#

1. Screen Name & Route: /admin/ai-evaluation — Content Evaluation Table. Role: ADMIN only.

2. Data to Show:

  • Table of all AI-generated content pieces across all clients, evaluated by quality.
  • Columns: content ID, client name, type (GBP post, social post, landing page, citation description, review reply, blog post), model used, quality score (0-100), human rating (approved/rejected/pending), generation time, cost, created at, actions.
  • Quality score: 0-100, color-coded (>=80 green, 60-79 amber, <60 red).
  • Human rating: APPROVED (green check), REJECTED (red X), PENDING (amber clock).
  • Content preview: First 80 characters of content, expandable to full text.
  • Actions: View (modal with full content), Re-evaluate (re-run quality scoring), Edit (open in editor), Approve/Reject (if pending).

3. Display Pattern:

  • Component: DataTable (enhanced for admin, same as client table).
  • Layout: Full-width table, no card wrapper.
  • Header row: paper-white bg, caption (12px, weight 500, uppercase, text-tertiary), border-bottom mist 1px.
  • Data rows: card-white bg, border-bottom mist 1px, hover row-hover.
  • Cell padding: 12px 16px.
  • Quality score: Circular mini progress (24px), color-coded stroke. Tooltip shows breakdown (grammar, relevance, tone, medical accuracy).
  • Human rating badge: radius-full, pill, color-coded.
  • Content preview: body-sm (14px), truncated at 80 chars. Click to expand inline (max 500 chars).
  • Model badge: Text badge with model name. claude-sonnetdeep-indigo bg, claude-haikusky-blue bg, gpt-4oforest-teal bg, gpt-4o-minimint bg.
  • Actions: Icon buttons (eye, refresh, edit, check, X). 20px icons, space-2 gap.
  • Filter bar: Above table. Content type multi-select, model dropdown, quality score range (min/max), human rating dropdown, client dropdown, date range, search (content text or client name).
  • Sortable: All columns except Actions. Default sort: createdAt DESC.
  • Pagination: 50/100/200 per page. Default 50.
  • Batch actions: Select rows → "Batch Approve", "Batch Reject", "Batch Re-evaluate", "Export CSV".

4. Backend Endpoints:

  • admin.getContentEvaluation (query, adminProcedure):
    • Zod input:
      z.object({
        page: z.number().default(1),
        pageSize: z.number().default(50),
        sortBy: z.enum(["createdAt", "qualityScore", "generationTime", "cost", "type"]).default("createdAt"),
        sortOrder: z.enum(["asc", "desc"]).default("desc"),
        filters: z.object({
          contentType: z.array(z.enum(["GBP_POST", "SOCIAL_POST", "LANDING_PAGE", "CITATION_DESCRIPTION", "REVIEW_REPLY", "BLOG_POST"])).optional(),
          model: z.array(z.enum(["claude-sonnet", "claude-haiku", "gpt-4o", "gpt-4o-mini"])).optional(),
          qualityScoreMin: z.number().min(0).max(100).optional(),
          qualityScoreMax: z.number().min(0).max(100).optional(),
          humanRating: z.array(z.enum(["APPROVED", "REJECTED", "PENDING"])).optional(),
          clientId: z.string().optional(),
          dateFrom: z.string().datetime().optional(),
          dateTo: z.string().datetime().optional(),
          search: z.string().optional(),
        }).optional(),
      })
      
    • Zod output:
      z.object({
        content: z.array(z.object({
          id: z.string().uuid(),
          clientId: z.string(),
          clientName: z.string(),
          type: z.enum(["GBP_POST", "SOCIAL_POST", "LANDING_PAGE", "CITATION_DESCRIPTION", "REVIEW_REPLY", "BLOG_POST"]),
          model: z.enum(["claude-sonnet", "claude-haiku", "gpt-4o", "gpt-4o-mini"]),
          content: z.string().max(5000), // Truncated on server if longer
          qualityScore: z.number().min(0).max(100),
          qualityBreakdown: z.object({
            grammar: z.number().min(0).max(100),
            relevance: z.number().min(0).max(100),
            tone: z.number().min(0).max(100),
            medicalAccuracy: z.number().min(0).max(100).optional(),
          }),
          humanRating: z.enum(["APPROVED", "REJECTED", "PENDING"]),
          generationTime: z.number(), // seconds
          cost: z.number(), // USD
          createdAt: z.string().datetime(),
        })),
        totalCount: z.number().int(),
        totalPages: z.number().int(),
      })
      
  • admin.reEvaluateContent (mutation, adminProcedure):
    • Zod input: z.object({ contentId: z.string().uuid() }).
    • Zod output: z.object({ success: z.boolean(), newQualityScore: z.number().min(0).max(100) }).
  • admin.batchEvaluateContent (mutation, adminProcedure):
    • Zod input: z.object({ contentIds: z.array(z.string().uuid()), action: z.enum(["APPROVE", "REJECT", "RE_EVALUATE"]) }).
    • Zod output: z.object({ success: z.boolean(), processedCount: z.number().int() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 60 seconds (refetchInterval: 60000).
  • Filter/sort/page change: Immediate refetch.
  • After mutation (approve/reject/re-evaluate): invalidateQueries for admin.getContentEvaluation.

6. Data Manipulations:

  • Quality score color: >= 80forest-teal, 60-79ember-orange, < 60error-red.
  • Quality breakdown tooltip: Hover over score circle → tooltip shows 4 sub-scores with progress bars.
  • Content preview truncation: content.slice(0, 80) + "...". Full content in view modal.
  • Generation time: ${generationTime.toFixed(1)}s.
  • Cost: $${cost.toFixed(3)}.
  • Date: date-fns formatDistanceToNow from ISO date.
  • Batch action count: Badge on batch action button showing selected count. "Approve 5 selected".
  • Search debounce: 300ms. Searches content text and clientName.

7. Why Structured This Way:

  • Evaluation table: AI generates hundreds of content pieces daily. Admin needs a scannable table to spot low-quality content.
  • Quality score circle: Visual at-a-glance quality indicator. Admin scans for red circles.
  • Human rating separate from quality score: Quality score is algorithmic. Human rating is the ground truth. Both shown side-by-side to detect scoring drift.
  • Model badge: If GPT-4o-mini content consistently scores lower, admin shifts usage to Claude Sonnet.
  • Batch actions: Admin reviews content in batches. "Approve 20 similar GBP posts" saves time.
  • Content preview: Admin doesn't need to open every piece to judge quality. 80 chars is enough for most decisions.
  • Medical accuracy sub-score: Only shown for medical clients. Hidden for non-medical to reduce clutter.

8. Interaction Flows:

  • View content: Click eye icon → modal opens with full content text, quality breakdown, client context, and generation metadata. "Re-evaluate" button in modal.
  • Re-evaluate: Click refresh icon → admin.reEvaluateContent mutation → score updates → toast "Re-evaluated: New score 85".
  • Approve: Click check icon → content.approve mutation (via admin router) → status changes to APPROVED → toast "Content approved".
  • Reject: Click X icon → content.reject mutation → status changes to REJECTED → toast "Content rejected".
  • Batch approve: Select 5 rows → click "Approve 5" in batch bar → confirmation → admin.batchEvaluateContent → all 5 approved → toast "5 content pieces approved".
  • Row click: Opens content detail modal (same as eye icon).
  • Filter sidebar: Slide-over from right. Filter pills above table. URL sync.
  • Export CSV: "Export" button → downloads CSV with all columns + quality breakdown.

9. Error States:

State Trigger UI Treatment Recovery
Empty No content generated "No AI content yet. Generate content from client dashboards." N/A
Loading Initial load Skeleton rows (10 rows) Data arrives
Error Query fails Inline error banner + retry button Retry
Partial Some columns fail Show available columns, "—" for failed N/A
Batch fail One item in batch fails Toast: "4 approved, 1 failed. Check error log." Retry failed

10. Role-Based Variations:

  • ADMIN: Full table, all actions, batch operations, all filters.
  • Other roles: No access. Redirected to /dashboard.

Widget 4.3 — Model Performance Comparison#

1. Screen Name & Route: /admin/ai-evaluation — Model Performance Comparison. Role: ADMIN only.

2. Data to Show:

  • Comparative analysis of AI models used across the platform.
  • Bar chart: Quality score by model (average across all content types).
  • Line chart: Cost per piece over time (30 days) by model.
  • Table: Per-model statistics — total pieces, avg quality, avg generation time, avg cost, approval rate, usage share.
  • Recommendation engine: Suggests optimal model per content type based on quality/cost ratio.

3. Display Pattern:

  • Component: Composite — BarChart + LineChart + DataTable.
  • Layout: 2-column grid (desktop). Left: bar chart (quality by model). Right: line chart (cost over time). Below: full-width table.
  • Bar chart: X-axis = model names. Y-axis = avg quality score (0-100). Grouped bars by content type.
  • Line chart: X-axis = days. Y-axis = cost per piece (USD). Multiple lines per model.
  • Table: 7 columns. Model name, total pieces, avg quality, avg gen time, avg cost, approval rate, usage share %.
  • Recommendation panel: Below table. Card with "Recommended Model per Content Type" table.

4. Backend Endpoints:

  • admin.getModelPerformance (query, adminProcedure):
    • Zod input: z.object({ period: z.enum(["7d", "30d", "90d"]).default("30d") }).
    • Zod output:
      z.object({
        models: z.array(z.object({
          name: z.string(),
          totalPieces: z.number().int(),
          avgQualityScore: z.number(),
          avgGenerationTime: z.number(),
          avgCost: z.number(),
          approvalRate: z.number(),
          usageShare: z.number(), // percentage
          qualityByContentType: z.array(z.object({ type: z.string(), avgQuality: z.number() })),
          costOverTime: z.array(z.object({ date: z.string(), avgCost: z.number() })),
        })),
        recommendations: z.array(z.object({
          contentType: z.string(),
          recommendedModel: z.string(),
          reason: z.string(),
          projectedSavings: z.number().optional(), // USD
        })),
      })
      
  • Auth: adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 5 minutes (refetchInterval: 300000).
  • Period change: Immediate refetch.

6. Data Manipulations:

  • Quality score formatting: avgQualityScore.toFixed(1).
  • Cost formatting: $${avgCost.toFixed(3)}.
  • Generation time: ${avgGenerationTime.toFixed(1)}s.
  • Approval rate: (approvalRate * 100).toFixed(1)}%.
  • Usage share: Pie chart segment sizes. (usageShare).toFixed(1)}%.
  • Recommendation sorting: By projected savings (highest first).
  • Bar chart colors: Model-specific consistent colors. Claude Sonnet = deep-indigo, Claude Haiku = sky-blue, GPT-4o = forest-teal, GPT-4o-mini = mint.

7. Why Structured This Way:

  • Model comparison: Different models excel at different tasks. Admin needs data to optimize model selection.
  • Quality vs. cost tradeoff: Claude Sonnet may score higher but cost 3x more than Haiku. Admin decides if the quality improvement is worth the cost.
  • Recommendation engine: Automated suggestions reduce admin cognitive load. "Use Haiku for GBP posts, Sonnet for landing pages" is actionable.
  • Cost over time: Detects cost drift. If GPT-4o pricing increases, admin sees the trend and can switch.
  • 5-minute polling: Model performance changes slowly. Hourly would be too slow to catch cost spikes.

8. Interaction Flows:

  • Bar chart hover: Tooltip shows exact quality score + piece count for that model/content type.
  • Line chart hover: Tooltip shows exact cost per piece for that model on that day.
  • Table row click: Opens model detail modal with per-content-type breakdown.
  • Recommendation apply: Click "Apply" on recommendation → opens confirmation → updates default model for that content type in prompt templates.
  • Export: "Export Report" button → PDF/CSV with all model performance data.

9. Error States:

State Trigger UI Treatment Recovery
Empty No model data "No model performance data yet. Content generation will populate this." N/A
Loading Initial load Skeleton charts + skeleton table Data arrives
Error Query fails Inline error with retry Retry

10. Role-Based Variations:

  • ADMIN: Full charts, table, recommendations, apply actions.
  • Other roles: No access.

Section 5 — Blog Manager (/admin/blog)#

Role access: ADMIN only. Layout: Content list (top) + editor (middle) + SEO panel (right). Sidebar: carbon (#12161E). Purpose: Manage the RankFlow AI blog — create, edit, publish, and optimize blog posts for SEO and lead generation. The blog drives organic traffic and positions RankFlow as a thought leader in local SEO for medical professionals.


Widget 5.1 — Blog Post List#

1. Screen Name & Route: /admin/blog — Blog Post List. Role: ADMIN only.

2. Data to Show:

  • Table of all blog posts: title, slug, status, author, publish date, SEO score, views (30d), leads generated, actions.
  • Status: DRAFT (slate), PENDING_REVIEW (amber), SCHEDULED (sky-blue), PUBLISHED (forest-teal), UNPUBLISHED (error-red).
  • SEO score: 0-100, color-coded. Based on keyword density, meta tags, readability, internal links, schema markup.
  • Views: 30-day page view count.
  • Leads generated: Number of lead form submissions from this post.
  • Actions: Edit, Preview, Publish/Unpublish, Delete, Duplicate.

3. Display Pattern:

  • Component: DataTable.
  • Layout: Full-width table.
  • Header row: Standard admin table header.
  • Data rows: card-white bg, hover row-hover.
  • Title column: body-sm (14px, weight 500), clickable (navigates to editor). Max 60 chars, truncated with ellipsis.
  • Slug: caption (12px, text-tertiary), below title. slug.slice(0, 40).
  • Status badge: radius-full, pill, color-coded.
  • SEO score: Circular progress (20px), color-coded. Tooltip: "SEO Score: 85/100 — Good keyword density, missing alt text on 2 images."
  • Views: body-sm, integer with comma. views.toLocaleString("en-IN").
  • Leads: body-sm, integer. Green if > 0.
  • Actions: Icon buttons (edit, eye, publish, trash, copy). Hidden until hover (desktop).
  • Filter bar: Status dropdown, author dropdown, date range, SEO score range, search (title/slug).
  • Sortable: All columns. Default: publishDate DESC (or createdAt DESC for drafts).
  • Pagination: 25/50/100 per page. Default 25.
  • Batch actions: Select rows → "Batch Publish", "Batch Unpublish", "Batch Delete", "Export CSV".
  • "New Post" button: Primary (Ember Orange), top-right of table. Opens editor with blank post.

4. Backend Endpoints:

  • admin.getBlogPosts (query, adminProcedure):
    • Zod input:
      z.object({
        page: z.number().default(1),
        pageSize: z.number().default(25),
        sortBy: z.enum(["createdAt", "publishDate", "seoScore", "views", "leads"]).default("createdAt"),
        sortOrder: z.enum(["asc", "desc"]).default("desc"),
        filters: z.object({
          status: z.array(z.enum(["DRAFT", "PENDING_REVIEW", "SCHEDULED", "PUBLISHED", "UNPUBLISHED"])).optional(),
          author: z.array(z.string()).optional(),
          seoScoreMin: z.number().min(0).max(100).optional(),
          seoScoreMax: z.number().min(0).max(100).optional(),
          dateFrom: z.string().datetime().optional(),
          dateTo: z.string().datetime().optional(),
          search: z.string().optional(),
        }).optional(),
      })
      
    • Zod output:
      z.object({
        posts: z.array(z.object({
          id: z.string().uuid(),
          title: z.string().max(200),
          slug: z.string().max(200),
          status: z.enum(["DRAFT", "PENDING_REVIEW", "SCHEDULED", "PUBLISHED", "UNPUBLISHED"]),
          author: z.object({ id: z.string(), name: z.string() }),
          publishDate: z.string().datetime().nullable(),
          seoScore: z.number().min(0).max(100),
          views30d: z.number().int(),
          leadsGenerated: z.number().int(),
          createdAt: z.string().datetime(),
          updatedAt: z.string().datetime(),
        })),
        totalCount: z.number().int(),
        totalPages: z.number().int(),
      })
      
  • admin.createBlogPost (mutation, adminProcedure):
    • Zod input: z.object({ title: z.string().min(1).max(200), slug: z.string().max(200) }).
    • Zod output: z.object({ id: z.string().uuid(), title: z.string(), slug: z.string() }).
  • admin.deleteBlogPost (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.duplicateBlogPost (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output: z.object({ id: z.string().uuid(), title: z.string(), slug: z.string() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 5 minutes (refetchInterval: 300000). Blog metrics change slowly.
  • Filter/sort/page change: Immediate refetch.
  • After mutation: invalidateQueries for admin.getBlogPosts.

6. Data Manipulations:

  • SEO score color: >= 80forest-teal, 60-79ember-orange, < 60error-red.
  • Views formatting: views30d.toLocaleString("en-IN").
  • Leads formatting: leadsGenerated.toLocaleString("en-IN"). If > 0, green text + "+N leads" badge.
  • Slug validation: slug must be URL-safe (lowercase, alphanumeric, hyphens only). Frontend regex: /^[a-z0-9-]+$/.
  • Duplicate slug: If duplicate, append -2, -3, etc. Server-side validation.
  • Date formatting: publishDatedate-fns format "dd MMM yyyy".
  • Search: Debounced 300ms. Searches title and slug.

7. Why Structured This Way:

  • Blog as admin function: The blog is a marketing asset, not a client feature. Only admin creates/edits blog posts.
  • SEO score prominent: Blog posts exist to drive SEO. SEO score tells admin if a post is optimized before publishing.
  • Leads generated: Ultimate metric. If a post generates 50 leads, admin writes more like it. If 0 leads, admin re-evaluates topic.
  • 25 per page default: Blog posts are fewer than clients. 25 is scannable. Admin sees titles and slugs clearly.
  • Duplicate function: Common workflow. Admin duplicates a high-performing post as a template for new content.
  • Batch publish: Admin writes multiple posts in draft, then publishes them together for a content push.

8. Interaction Flows:

  • New post: Click "New Post" → admin.createBlogPost mutation → navigates to /admin/blog/[id]/edit with blank post.
  • Edit: Click edit icon → navigates to /admin/blog/[id]/edit.
  • Preview: Click eye icon → opens post preview in new tab (/blog/[slug]?preview=true).
  • Publish: Click publish icon (if draft) → confirmation → admin.publishBlogPost → status changes to PUBLISHED → toast "Post published".
  • Unpublish: Click unpublish icon (if published) → confirmation → status changes to UNPUBLISHED → toast "Post unpublished".
  • Delete: Click trash icon → confirmation modal + "Type post title to confirm" → admin.deleteBlogPost → toast "Post deleted".
  • Duplicate: Click copy icon → admin.duplicateBlogPost → new post appears in list with "(Copy)" suffix → toast "Post duplicated".
  • Batch publish: Select 3 drafts → "Publish 3" → confirmation → all published → toast.
  • Row click: Navigates to editor.

9. Error States:

State Trigger UI Treatment Recovery
Empty No blog posts "No blog posts yet. Create your first post." + "New Post" CTA Create post
Loading Initial load Skeleton rows Data arrives
Error Query fails Inline error + retry Retry
Duplicate slug Slug conflict Inline error: "Slug already exists." Change slug

10. Role-Based Variations:

  • ADMIN: Full list, all actions, batch operations.
  • Other roles: No access. Redirected to /dashboard.

Widget 5.2 — Blog Editor#

1. Screen Name & Route: /admin/blog/[id]/edit — Blog Editor. Role: ADMIN only.

2. Data to Show:

  • Full blog post editor with SEO optimization panel.
  • Content editor: Rich text (TipTap) with headings, lists, links, images, embeds, code blocks.
  • SEO panel (right sidebar): Meta title, meta description, focus keyword, slug, readability score, keyword density, internal link suggestions, schema markup toggle.
  • AI assistant: "Generate outline", "Expand section", "Rewrite for SEO", "Generate meta description" buttons.
  • Publish controls: Save draft, Schedule (date-time picker), Publish now, Unpublish.
  • Preview: Split-screen or tabbed preview of rendered post.
  • Version history: Last 10 versions, diff view, restore.

3. Display Pattern:

  • Component: BlogEditor (composite: TipTap editor + SEO panel + AI assistant + publish controls).
  • Layout: 3-column (desktop): editor (left, 60%), preview (middle, 25%), SEO panel (right, 15%). On tablet: editor + SEO panel (preview in tab). On mobile: tabbed (editor / preview / SEO).
  • Editor: TipTap rich text. Toolbar: H1, H2, H3, bold, italic, link, bullet list, numbered list, image upload, code block, quote, horizontal rule. Toolbar sticky at top of editor area.
  • SEO panel: Card with sections:
    • Meta title: Input, max 60 chars, counter. Green if <= 60, amber if 61-70, red if > 70.
    • Meta description: Textarea, max 160 chars, counter. Same color logic.
    • Focus keyword: Input. Shows keyword density in content (%). Suggests if too low (< 0.5%) or too high (> 2.5%).
    • Slug: Input, URL-safe validation. Auto-generated from title if blank.
    • Readability score: 0-100. Based on Flesch-Kincaid. >= 60 = easy, 40-59 = moderate, < 40 = difficult. Color-coded.
    • Keyword density: Progress bar showing density in content. Green 0.5-2.5%, amber < 0.5%, red > 2.5%.
    • Internal links: Suggests existing blog posts to link to. Shows count of internal links in post.
    • Schema markup: Toggle for Article schema. Auto-generates JSON-LD.
    • SEO score: Overall score (0-100), circular progress. Updates live as content changes.
  • AI assistant panel: Below SEO panel. Buttons: "Generate Outline" (from title), "Expand Section" (cursor position), "Rewrite for SEO" (selection), "Generate Meta Description" (from content). Each button triggers content.generate with appropriate task type.
  • Publish controls: Sticky bar at bottom. "Save Draft" (secondary), "Schedule" (secondary + date picker), "Publish" (primary, Ember Orange), "Unpublish" (danger ghost if published).
  • Version history: Collapsible section at bottom of SEO panel. "Last saved: 2 min ago by Admin". Click "History" → list of versions with timestamp, author, action. Click version → diff view (side-by-side). "Restore" button on old version.
  • Preview: Renders post with RankFlow blog CSS. Updates on debounce (500ms after typing stops).
  • Auto-save: Every 30 seconds. Toast: "Auto-saved". No manual save required for drafts.

4. Backend Endpoints:

  • admin.getBlogPost (query, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output:
      z.object({
        id: z.string().uuid(),
        title: z.string().max(200),
        slug: z.string().max(200),
        content: z.string().max(50000), // HTML
        status: z.enum(["DRAFT", "PENDING_REVIEW", "SCHEDULED", "PUBLISHED", "UNPUBLISHED"]),
        author: z.object({ id: z.string(), name: z.string() }),
        metaTitle: z.string().max(60),
        metaDescription: z.string().max(160),
        focusKeyword: z.string().max(100),
        schemaMarkup: z.boolean().default(false),
        publishDate: z.string().datetime().nullable(),
        scheduledDate: z.string().datetime().nullable(),
        seoScore: z.number().min(0).max(100),
        versionHistory: z.array(z.object({
          version: z.number().int(),
          content: z.string(),
          author: z.string(),
          action: z.string(),
          createdAt: z.string().datetime(),
        })).max(10),
      })
      
  • admin.updateBlogPost (mutation, adminProcedure):
    • Zod input:
      z.object({
        id: z.string().uuid(),
        title: z.string().max(200).optional(),
        slug: z.string().max(200).optional(),
        content: z.string().max(50000).optional(),
        metaTitle: z.string().max(60).optional(),
        metaDescription: z.string().max(160).optional(),
        focusKeyword: z.string().max(100).optional(),
        schemaMarkup: z.boolean().optional(),
        scheduledDate: z.string().datetime().optional(),
      })
      
    • Zod output: Updated blog post object.
  • admin.publishBlogPost (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid(), publishNow: z.boolean().default(true) }).
    • Zod output: z.object({ success: z.boolean(), url: z.string().url() }).
  • admin.unpublishBlogPost (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.restoreBlogVersion (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid(), version: z.number().int() }).
    • Zod output: z.object({ success: z.boolean(), content: z.string() }).
  • content.generate (mutation, practiceProcedure — admin uses admin.executeSkill or admin-specific content generation endpoint):
    • For AI assistant features: admin.generateContent (mutation, adminProcedure):
      • Zod input: z.object({ taskType: z.enum(["BLOG_OUTLINE", "BLOG_EXPAND", "BLOG_REWRITE", "META_DESCRIPTION"]), content: z.string(), context: z.string().optional() }).
      • Zod output: z.object({ generatedContent: z.string(), modelUsed: z.string(), cost: z.number() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Post data: On mount. Static during editing (auto-save updates server, no refetch needed).
  • SEO score: Recalculated client-side on every keystroke (debounced 500ms). No server call.
  • AI generation: On-demand only.
  • Version history: On mount + after each save.

6. Data Manipulations:

  • SEO score calculation (client-side):
    • Meta title length: 30% weight. 50-60 chars = 100%, 40-49 or 61-70 = 80%, < 40 or > 70 = 50%.
    • Meta description length: 20% weight. 150-160 chars = 100%, 130-149 or 161-170 = 80%, < 130 or > 170 = 50%.
    • Keyword density: 20% weight. 0.5-2.5% = 100%, < 0.5% = 60%, > 2.5% = 50%.
    • Readability: 15% weight. >= 60 = 100%, 40-59 = 80%, < 40 = 50%.
    • Internal links: 10% weight. >= 2 = 100%, 1 = 80%, 0 = 50%.
    • Schema markup: 5% weight. Present = 100%, absent = 0%.
    • Total: Weighted average of above. Math.round(total).
  • Keyword density: Count occurrences of focusKeyword in content (case-insensitive, whole word). (occurrences / totalWords * 100).toFixed(2).
  • Readability score: Flesch-Kincaid reading ease. 206.835 - 1.015 * (totalWords / totalSentences) - 84.6 * (totalSyllables / totalWords). Simplified estimation on frontend; server computes exact score on save.
  • Internal link count: Count <a> tags in content where href starts with /blog/ or /.
  • Slug generation: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''). Auto-generated if slug field is blank.
  • Auto-save debounce: 30 seconds after last keystroke. Also triggered on blur of any field.
  • Preview debounce: 500ms after last keystroke. Renders HTML in preview iframe.
  • Character counters: Live count on meta title, meta description, focus keyword. Color changes at thresholds.

7. Why Structured This Way:

  • 3-column layout: Editor needs space, preview needs context, SEO panel needs visibility. 60/25/15 split is optimal for content creation.
  • Live SEO score: Admin sees SEO impact immediately as they type. No "save and check" cycle. This trains good SEO habits.
  • AI assistant in sidebar: Contextual help without blocking the editor. "Expand section" uses cursor position. "Rewrite for SEO" uses text selection.
  • Auto-save every 30s: Prevents data loss. Admin doesn't need to remember to save. Toast confirms save without interrupting flow.
  • Version history: Content editing is iterative. If admin makes a mistake, they can restore. Diff view shows exactly what changed.
  • Keyword density bar: Visual feedback on keyword stuffing. Green = optimal, amber = too few, red = too many. This is the most common SEO mistake.
  • Schema markup toggle: RankFlow's platform automatically adds schema. Blog posts should too. One toggle enables Article schema.
  • Internal link suggestions: SEO best practice. Suggests existing posts to link to. Increases site authority and reduces bounce rate.

8. Interaction Flows:

  • Type in editor: TipTap captures input. SEO panel recalculates scores (debounced 500ms). Preview updates (debounced 500ms). Auto-save timer resets.
  • Click "Generate Outline": AI assistant sends title to admin.generateContent with taskType: "BLOG_OUTLINE" → returns H2/H3 structure → inserts at cursor position.
  • Select text + "Rewrite for SEO": Selected text sent to AI → returns optimized version → replaces selection.
  • Click "Publish": If SEO score < 60, warning modal: "SEO score is 45. Publish anyway?" → confirmation → admin.publishBlogPost → toast "Published! Live at [URL]".
  • Click "Schedule": Date-time picker opens → select date → admin.updateBlogPost with scheduledDate → status changes to SCHEDULED → toast "Scheduled for [date]".
  • Auto-save: After 30s of inactivity → admin.updateBlogPost mutation → toast "Auto-saved" → version history updates.
  • Restore version: Click old version in history → diff view opens → "Restore" button → content replaced → toast "Restored to version N".
  • Preview tab: Click "Preview" tab (mobile) → full-width preview of rendered post.

9. Error States:

State Trigger UI Treatment Recovery
Post not found Invalid ID 404 page: "Blog post not found" Navigate to blog list
Load error admin.getBlogPost fails Full error page with retry Retry
Save fail admin.updateBlogPost fails Toast: "Auto-save failed. Please check connection." + manual "Save" button Retry
Slug invalid Regex fail Inline error: "Slug must be URL-safe (lowercase letters, numbers, hyphens)." Fix slug
SEO score warning < 60 on publish Confirmation modal: "SEO score is low. Publish anyway?" Confirm or improve SEO
AI generation fail LLM timeout Toast: "AI generation failed. Please try again or write manually." Retry or manual

10. Role-Based Variations:

  • ADMIN: Full editor, all AI features, publish, schedule, unpublish, delete, version restore.
  • Other roles: No access. Redirected to /dashboard.

Section 6 — Prompt Manager (/admin/prompts)#

Role access: ADMIN only. Layout: Prompt list (top) + editor (modal) + version history (side panel). Sidebar: carbon (#12161E). Purpose: Manage AI prompt templates used across the platform. Each content type (GBP post, social post, citation description, review reply, landing page, blog post, FAQ) has a prompt template. Admin can edit, A/B test, and version control prompts to optimize AI output quality.


Widget 6.1 — Prompt Template List#

1. Screen Name & Route: /admin/prompts — Prompt Template List. Role: ADMIN only.

2. Data to Show:

  • Table of all prompt templates: task type, name, model, avg quality score, usage count (24h), last modified, status, actions.
  • Task type: GBP_POST, SOCIAL_POST, CITATION_DESCRIPTION, REVIEW_REPLY, LANDING_PAGE, BLOG_POST, FAQ, META_DESCRIPTION, SCHEMA_MARKUP.
  • Model: claude-sonnet, claude-haiku, gpt-4o, gpt-4o-mini.
  • Avg quality score: Quality score of content generated with this prompt (last 30 days).
  • Usage count: Number of times this prompt was used in last 24 hours.
  • Status: ACTIVE (green), DRAFT (amber), ARCHIVED (slate), A_B_TEST (blue).
  • Actions: Edit, Preview (test with sample data), A/B Test (duplicate for testing), Archive, Activate.

3. Display Pattern:

  • Component: DataTable.
  • Layout: Full-width table.
  • Header row: Standard admin table header.
  • Data rows: card-white bg, hover row-hover.
  • Task type column: Badge with icon. GBP_POSTMapPin, SOCIAL_POSTShare2, CITATION_DESCRIPTIONLink, etc.
  • Name column: body-sm (14px, weight 500), truncated at 50 chars.
  • Model badge: Same colors as content evaluation table (claude-sonnet = deep-indigo, etc.).
  • Quality score: Circular progress (20px), color-coded. Same thresholds as content evaluation.
  • Usage count: body-sm, integer. usageCount.toLocaleString("en-IN").
  • Status badge: radius-full, pill, color-coded.
  • Actions: Icon buttons (edit, play, split, archive, activate). Hidden until hover (desktop).
  • Filter bar: Task type dropdown, model dropdown, status dropdown, quality score range, search.
  • Sortable: All columns. Default: usageCount DESC (most used first).
  • Pagination: 25/50 per page. Default 25.
  • "New Prompt" button: Primary (Ember Orange), top-right. Opens editor with blank template.
  • A/B test indicator: If status is A_B_TEST, row has sky-blue left border and "A/B Test" badge.

4. Backend Endpoints:

  • admin.getPrompts (query, adminProcedure):
    • Zod input:
      z.object({
        page: z.number().default(1),
        pageSize: z.number().default(25),
        sortBy: z.enum(["taskType", "model", "qualityScore", "usageCount", "lastModified"]).default("usageCount"),
        sortOrder: z.enum(["asc", "desc"]).default("desc"),
        filters: z.object({
          taskType: z.array(z.enum(["GBP_POST", "SOCIAL_POST", "CITATION_DESCRIPTION", "REVIEW_REPLY", "LANDING_PAGE", "BLOG_POST", "FAQ", "META_DESCRIPTION", "SCHEMA_MARKUP"])).optional(),
          model: z.array(z.enum(["claude-sonnet", "claude-haiku", "gpt-4o", "gpt-4o-mini"])).optional(),
          status: z.array(z.enum(["ACTIVE", "DRAFT", "ARCHIVED", "A_B_TEST"])).optional(),
          qualityScoreMin: z.number().min(0).max(100).optional(),
          search: z.string().optional(),
        }).optional(),
      })
      
    • Zod output:
      z.object({
        prompts: z.array(z.object({
          id: z.string().uuid(),
          taskType: z.enum(["GBP_POST", "SOCIAL_POST", "CITATION_DESCRIPTION", "REVIEW_REPLY", "LANDING_PAGE", "BLOG_POST", "FAQ", "META_DESCRIPTION", "SCHEMA_MARKUP"]),
          name: z.string().max(100),
          model: z.enum(["claude-sonnet", "claude-haiku", "gpt-4o", "gpt-4o-mini"]),
          systemPrompt: z.string().max(10000),
          userPromptTemplate: z.string().max(10000),
          qualityScore: z.number().min(0).max(100),
          usageCount24h: z.number().int(),
          usageCount30d: z.number().int(),
          status: z.enum(["ACTIVE", "DRAFT", "ARCHIVED", "A_B_TEST"]),
          lastModified: z.string().datetime(),
          modifiedBy: z.string(),
          abTestVariant: z.string().optional(), // If A/B test, links to parent prompt
        })),
        totalCount: z.number().int(),
        totalPages: z.number().int(),
      })
      
  • admin.createPrompt (mutation, adminProcedure):
    • Zod input: z.object({ taskType: z.enum([...]), name: z.string().min(1).max(100), model: z.enum([...]), systemPrompt: z.string().max(10000), userPromptTemplate: z.string().max(10000) }).
    • Zod output: z.object({ id: z.string().uuid(), name: z.string() }).
  • admin.updatePrompt (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid(), name: z.string().optional(), systemPrompt: z.string().optional(), userPromptTemplate: z.string().optional(), model: z.enum([...]).optional(), status: z.enum([...]).optional() }).
    • Zod output: Updated prompt object.
  • admin.archivePrompt (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.activatePrompt (mutation, adminProcedure):
    • Zod input: z.object({ id: z.string().uuid() }).
    • Zod output: z.object({ success: z.boolean() }).
  • admin.createABTest (mutation, adminProcedure):
    • Zod input: z.object({ basePromptId: z.string().uuid(), variantName: z.string(), systemPrompt: z.string().optional(), userPromptTemplate: z.string().optional(), model: z.enum([...]).optional() }).
    • Zod output: z.object({ id: z.string().uuid(), variantId: z.string().uuid() }).
  • admin.testPrompt (mutation, adminProcedure):
    • Zod input: z.object({ promptId: z.string().uuid(), sampleVariables: z.record(z.any()) }).
    • Zod output: z.object({ generatedContent: z.string(), qualityScore: z.number(), generationTime: z.number(), cost: z.number() }).
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Initial load: On mount.
  • Polling: Every 5 minutes (refetchInterval: 300000).
  • Filter/sort/page change: Immediate refetch.
  • After mutation: invalidateQueries for admin.getPrompts.

6. Data Manipulations:

  • Quality score color: >= 80forest-teal, 60-79ember-orange, < 60error-red.
  • Usage count: usageCount24h.toLocaleString("en-IN") + usageCount30d.toLocaleString("en-IN") in tooltip.
  • Task type icon mapping: Same as content evaluation table.
  • Status color: ACTIVEforest-teal, DRAFTember-orange, ARCHIVEDslate, A_B_TESTsky-blue.
  • A/B test row: Left border 3px sky-blue. Badge "A/B Test".
  • Search: Debounced 300ms. Searches name, taskType, systemPrompt (first 200 chars).

7. Why Structured This Way:

  • Prompt management is core moat: RankFlow's differentiation is AI-generated content. Prompt quality directly impacts client satisfaction. This screen is where the platform's quality is controlled.
  • Quality score per prompt: Admin sees which prompts produce the best content. Low-quality prompts are flagged for revision.
  • Usage count: If a prompt is used 500 times/day but quality is declining, admin needs to act fast. High usage + low quality = urgent.
  • A/B testing: Admin can test prompt variations. "Does adding 'use medical terminology' to the system prompt improve medical content quality?" A/B test answers this.
  • Model per prompt: Each prompt can specify its own model. GBP posts use Haiku (cheap, fast). Landing pages use Sonnet (high quality, expensive). This is the cost optimization lever.
  • Archive vs. delete: Prompts are never deleted — they are archived. Audit trail preserves all prompt versions. This is critical for compliance and debugging.

8. Interaction Flows:

  • New prompt: Click "New Prompt" → modal opens with blank form → select task type, name, model → system prompt textarea, user prompt template textarea → "Save Draft" or "Activate".
  • Edit: Click edit icon → modal opens with current prompt data → edit fields → "Save" → admin.updatePrompt → toast "Prompt updated".
  • Preview (test): Click play icon → "Test Prompt" modal opens → sample variables form (auto-populated from task type defaults) → "Run Test" → admin.testPrompt → result shows generated content, quality score, generation time, cost → admin evaluates output.
  • A/B test: Click split icon → "Create A/B Test" modal → enter variant name, optionally modify system prompt or user template → "Create" → admin.createABTest → new variant appears with A_B_TEST status → both variants run simultaneously, 50/50 traffic split → quality scores compared over time.
  • Archive: Click archive icon → confirmation → admin.archivePrompt → status changes to ARCHIVED → toast "Prompt archived".
  • Activate: Click activate icon (if draft/archived) → admin.activatePrompt → status changes to ACTIVE → toast "Prompt activated".
  • Row click: Opens prompt detail modal with full prompt text, version history, and usage stats.

9. Error States:

State Trigger UI Treatment Recovery
Empty No prompts "No prompts configured. Create your first prompt." + "New Prompt" CTA Create prompt
Loading Initial load Skeleton rows Data arrives
Error Query fails Inline error + retry Retry
Test fail admin.testPrompt fails "Test failed. Check prompt variables or model availability." Retry or edit prompt
A/B test fail Variant creation fails "A/B test creation failed." Retry

10. Role-Based Variations:

  • ADMIN: Full list, all actions, test, A/B test, archive, activate.
  • Other roles: No access. Redirected to /dashboard.

Widget 6.2 — Prompt Editor#

1. Screen Name & Route: /admin/prompts (modal) — Prompt Editor. Role: ADMIN only.

2. Data to Show:

  • Full prompt editor with system prompt and user prompt template.
  • System prompt: Textarea, max 10000 chars. Instructions to the AI model (persona, tone, constraints, format).
  • User prompt template: Textarea, max 10000 chars. Template with variable placeholders ({{practiceName}}, {{services}}, {{location}}, etc.).
  • Variable extractor: Parses {{variableName}} from template and shows list of required variables.
  • Model selector: Dropdown of available models (Claude Sonnet, Haiku, GPT-4o, GPT-4o-mini).
  • Temperature slider: 0.0–1.0, step 0.1. Default 0.7. Tooltip explains each value.
  • Max tokens input: Number, default 2000, max 8000.
  • Version history: Last 10 versions with diff view.
  • Test panel: Side panel with sample variables and "Run Test" button. Shows generated output.

3. Display Pattern:

  • Component: Modal (large, 960px max-width, 85vh height).
  • Header: "Edit Prompt: [Prompt Name]" (heading-sm, 24px, weight 500) + close button + "Save" (primary) + "Save & Test" (secondary).
  • Body: 2-column layout (desktop). Left: prompt editor (60%). Right: config panel + test panel (40%).
  • Left column:
    • Task type badge (non-editable).
    • Name input: body (16px), max 100 chars.
    • System prompt textarea: body-sm (14px), monospace font, 300px min-height. Line numbers optional. Syntax highlighting for {{variables}} (blue color).
    • User prompt template textarea: Same styling as system prompt. 300px min-height.
    • Variable list: Below template. Chips showing extracted {{variables}}. Color: sky-blue bg, deep-indigo text.
  • Right column:
    • Model selector: Dropdown with model names + cost-per-1k-tokens badge.
    • Temperature: Slider input (0.0–1.0). Labels below: "Precise (0.0)" — "Balanced (0.7)" — "Creative (1.0)". Tooltip on hover.
    • Max tokens: Number input, 100–8000. Default 2000.
    • Status: Dropdown (ACTIVE, DRAFT, ARCHIVED).
    • Test panel: Collapsible section. Sample variables form (auto-generated from template variables). "Run Test" button. Output area (collapsible, max 400px, scrollable).
  • Footer: "Cancel" (ghost), "Save Draft" (secondary), "Save & Activate" (primary, Ember Orange).
  • Version history: Collapsible at bottom of left column. List of versions with timestamp, author, action. Click → diff view.

4. Backend Endpoints:

  • Same as Widget 6.1: admin.getPrompts, admin.updatePrompt, admin.createPrompt, admin.testPrompt.
  • Additional: admin.getPromptVersions (query, adminProcedure):
    • Zod input: z.object({ promptId: z.string().uuid() }).
    • Zod output:
      z.array(z.object({
        version: z.number().int(),
        systemPrompt: z.string(),
        userPromptTemplate: z.string(),
        model: z.string(),
        temperature: z.number(),
        maxTokens: z.number(),
        author: z.string(),
        action: z.string(),
        createdAt: z.string().datetime(),
      })).max(10)
      
  • Auth: All adminProcedure.

5. Fetch Frequency:

  • Prompt data: On modal open. Static during editing.
  • Test output: On-demand only (when "Run Test" clicked).
  • Version history: On modal open.

6. Data Manipulations:

  • Variable extraction: Regex \{\{([a-zA-Z0-9_]+)\}\} finds all variables in template. Unique list shown as chips. If variable is missing from sample data, chip is amber (warning).
  • Temperature slider: temperature value stored as number. Display: temperature.toFixed(1).
  • Max tokens: maxTokens integer. Validation: 100–8000.
  • Cost badge: Next to model selector. costPer1kTokens in USD. "$0.003 per 1K tokens" for Claude Haiku, "$0.015 per 1K tokens" for Claude Sonnet.
  • Diff view: diff library or simple text comparison. Shows + (added) in green, - (removed) in red.
  • Test output formatting: Generated content rendered as plain text in scrollable area. Quality score, generation time, cost shown below.

7. Why Structured This Way:

  • Large modal: Prompts are text-heavy. 960px width and 85vh height give ample space for long prompts.
  • Monospace font: Prompts are code-like. Monospace preserves formatting and makes variable placeholders stand out.
  • Variable extraction: Admin doesn't need to manually track variables. System extracts them automatically. Missing variables are flagged before testing.
  • Test panel in modal: Admin edits prompt → runs test → evaluates output → iterates. All in one modal. No context switching.
  • Temperature slider: Visual control over creativity vs. precision. Slider is more intuitive than a number input.
  • Cost badge: Cost awareness. Admin sees that Sonnet is 5x more expensive than Haiku. Informs model selection.
  • Version history: Prompts are refined over time. If a change breaks quality, admin can restore. Diff view shows exactly what changed.
  • Save & Test: Common workflow. Admin saves then immediately tests. Combined button reduces clicks.

8. Interaction Flows:

  • Open modal: Click edit icon → admin.getPrompts (or cached data) → modal opens with prompt data.
  • Edit system prompt: Type in textarea. Variable syntax {{var}} highlighted in blue. No server call.
  • Edit user template: Type in textarea. Variables extracted in real-time. Chips appear below. If unknown variable, chip is amber with tooltip "Variable not in system.".
  • Change model: Dropdown selection. Cost badge updates. If switching from Haiku to Sonnet, badge shows higher cost.
  • Adjust temperature: Drag slider. Value updates. Tooltip shows "Current: 0.7 — Balanced".
  • Run test: Fill sample variables (auto-generated form) → click "Run Test" → admin.testPrompt → loading spinner → output appears in test panel with quality score, time, cost → admin evaluates.
  • Save: Click "Save" → admin.updatePrompt → toast "Prompt saved" → version history updates → modal closes.
  • Save & Test: Click "Save & Test" → saves prompt → immediately runs test with current sample variables → shows output.
  • Restore version: Click old version in history → diff view → "Restore" → replaces current prompt with old version → toast "Restored to version N".

9. Error States:

State Trigger UI Treatment Recovery
Prompt too long > 10000 chars Inline error: "Prompt exceeds 10,000 character limit." Trim prompt
Invalid variable {{var}} with spaces or special chars Highlight in red with tooltip: "Invalid variable name. Use only letters, numbers, underscores." Fix variable
Missing variable Test variable not provided Amber chip: "Variable '{{name}}' missing from test data." Fill test data
Test fail Model error or timeout Test panel shows error: "Test failed: [message]" Retry or check model
Save fail API error Toast: "Failed to save prompt. Please try again." Retry

10. Role-Based Variations:

  • ADMIN: Full editor, all models, test, save, activate, archive, version restore.
  • Other roles: No access.

End of Admin Spec Part 2#

Next: Admin Spec Part 3 covers Sections 7–10 (System Configuration, Alert Manager, Audit & Logs, Data Governance).