Frontend Specs
RankFlow AI — Admin Dashboard Frontend Specification (Part 2)
Document Version: 1.0.0
docs/specs/frontend/admin-spec-02-ai-blog-prompts.mdOn this page
- Design Token Compliance (Inherited)
- Section 4 — AI Evaluation (/admin/ai-evaluation)
- Widget 4.1 — AI Quality KPI Cards
- Widget 4.2 — Content Evaluation Table
- Widget 4.3 — Model Performance Comparison
- Section 5 — Blog Manager (/admin/blog)
- Widget 5.1 — Blog Post List
- Widget 5.2 — Blog Editor
- Section 6 — Prompt Manager (/admin/prompts)
- Widget 6.1 — Prompt Template List
- Widget 6.2 — Prompt Editor
- End of Admin Spec Part 2
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
/dashboardwith 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, padding20px. - 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-evaluationmount. - 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-orangewarning. Hallucination > 2% →error-redcritical. Rejection > 15% →error-redalert. - 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-whitebg,caption(12px, weight 500, uppercase,text-tertiary), border-bottommist1px. - Data rows:
card-whitebg, border-bottommist1px, hoverrow-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-sonnet→deep-indigobg,claude-haiku→sky-bluebg,gpt-4o→forest-tealbg,gpt-4o-mini→mintbg. - Actions: Icon buttons (eye, refresh, edit, check, X). 20px icons,
space-2gap. - 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(), })
- Zod input:
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) }).
- Zod input:
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() }).
- Zod input:
- 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):
invalidateQueriesforadmin.getContentEvaluation.
6. Data Manipulations:
- Quality score color:
>= 80→forest-teal,60-79→ember-orange,< 60→error-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-fnsformatDistanceToNowfrom ISO date. - Batch action count: Badge on batch action button showing selected count. "Approve 5 selected".
- Search debounce: 300ms. Searches
contenttext andclientName.
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.reEvaluateContentmutation → score updates → toast "Re-evaluated: New score 85". - Approve: Click check icon →
content.approvemutation (via admin router) → status changes to APPROVED → toast "Content approved". - Reject: Click X icon →
content.rejectmutation → 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 })), })
- Zod input:
- 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-whitebg, hoverrow-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(), })
- Zod input:
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() }).
- Zod input:
admin.deleteBlogPost(mutation,adminProcedure):- Zod input:
z.object({ id: z.string().uuid() }). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
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() }).
- Zod input:
- 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:
invalidateQueriesforadmin.getBlogPosts.
6. Data Manipulations:
- SEO score color:
>= 80→forest-teal,60-79→ember-orange,< 60→error-red. - Views formatting:
views30d.toLocaleString("en-IN"). - Leads formatting:
leadsGenerated.toLocaleString("en-IN"). If > 0, green text + "+N leads" badge. - Slug validation:
slugmust 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:
publishDate→date-fnsformat "dd MMM yyyy". - Search: Debounced 300ms. Searches
titleandslug.
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.createBlogPostmutation → navigates to/admin/blog/[id]/editwith 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.generatewith 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), })
- Zod input:
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.
- Zod input:
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() }).
- Zod input:
admin.unpublishBlogPost(mutation,adminProcedure):- Zod input:
z.object({ id: z.string().uuid() }). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
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() }).
- Zod input:
content.generate(mutation,practiceProcedure— admin usesadmin.executeSkillor 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() }).
- Zod input:
- For AI assistant features:
- 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
focusKeywordin 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 wherehrefstarts 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
titletoadmin.generateContentwithtaskType: "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.updateBlogPostwithscheduledDate→ status changes to SCHEDULED → toast "Scheduled for [date]". - Auto-save: After 30s of inactivity →
admin.updateBlogPostmutation → 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-whitebg, hoverrow-hover. - Task type column: Badge with icon.
GBP_POST→MapPin,SOCIAL_POST→Share2,CITATION_DESCRIPTION→Link, 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 hassky-blueleft 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(), })
- Zod input:
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() }).
- Zod input:
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.
- Zod input:
admin.archivePrompt(mutation,adminProcedure):- Zod input:
z.object({ id: z.string().uuid() }). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
admin.activatePrompt(mutation,adminProcedure):- Zod input:
z.object({ id: z.string().uuid() }). - Zod output:
z.object({ success: z.boolean() }).
- Zod input:
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() }).
- Zod input:
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() }).
- Zod input:
- Auth: All
adminProcedure.
5. Fetch Frequency:
- Initial load: On mount.
- Polling: Every 5 minutes (
refetchInterval: 300000). - Filter/sort/page change: Immediate refetch.
- After mutation:
invalidateQueriesforadmin.getPrompts.
6. Data Manipulations:
- Quality score color:
>= 80→forest-teal,60-79→ember-orange,< 60→error-red. - Usage count:
usageCount24h.toLocaleString("en-IN")+usageCount30d.toLocaleString("en-IN")in tooltip. - Task type icon mapping: Same as content evaluation table.
- Status color:
ACTIVE→forest-teal,DRAFT→ember-orange,ARCHIVED→slate,A_B_TEST→sky-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 withA_B_TESTstatus → 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-bluebg,deep-indigotext.
- 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)
- Zod input:
- 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:
temperaturevalue stored as number. Display:temperature.toFixed(1). - Max tokens:
maxTokensinteger. Validation: 100–8000. - Cost badge: Next to model selector.
costPer1kTokensin USD. "$0.003 per 1K tokens" for Claude Haiku, "$0.015 per 1K tokens" for Claude Sonnet. - Diff view:
difflibrary 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).