Help Improvement 2 — “Was This Helpful?” Ratings
Status: [To Build]
Purpose: Collect per-topic thumbs-up / thumbs-down signals so we know which help topics need rewriting. Currently there is zero feedback mechanism — content quality is invisible.
User Experience
A small footer at the bottom of every HelpPage (both standalone /help/{slug} routes and inside the drawer):
──────────────────────────────────────
Was this helpful? 👍 👎
──────────────────────────────────────After clicking either thumb:
──────────────────────────────────────
Thanks for your feedback.
──────────────────────────────────────State resets on slug change. One rating per page per session — clicking again replaces the previous rating.
Data Model
Add to packages/db/prisma/schema.prisma:
model HelpPageRating {
id String @id @default(cuid())
slug String
rating HelpRating
tenantId String
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([slug])
@@index([tenantId])
@@map("help_page_rating")
}
enum HelpRating {
helpful
not_helpful
}API Endpoint
POST /tenant/v1/help/rating
// Request
{ slug: string; rating: "helpful" | "not_helpful" }
// Response
{ ok: true }Implementation: upsert on (tenantId, slug) — replaces the previous rating if one exists for this session.
// apps/api/src/routers/help.ts
fastify.post("/tenant/v1/help/rating", {
preHandler: [requireTenantUser],
schema: {
body: { type: "object", required: ["slug", "rating"],
properties: {
slug: { type: "string" },
rating: { type: "string", enum: ["helpful", "not_helpful"] },
},
},
response: { 200: { type: "object", additionalProperties: true } },
},
async handler(req, reply) {
const { slug, rating } = req.body as { slug: string; rating: "helpful" | "not_helpful" };
const tenantId = req.user.tenantId;
await db.helpPageRating.upsert({
where: { tenantId_slug: { tenantId, slug } },
update: { rating, createdAt: new Date() },
create: { slug, rating, tenantId },
});
reply.send({ ok: true });
},
});Note: requires @@unique([tenantId, slug]) on the model for the upsert where clause.
UI Component (packages/ui/src/HelpRating.tsx)
"use client";
import { useState } from "react";
import { ThumbsUp, ThumbsDown } from "lucide-react";
interface HelpRatingProps {
slug: string;
onRate: (slug: string, rating: "helpful" | "not_helpful") => Promise<void>;
}
export function HelpRating({ slug, onRate }: HelpRatingProps) {
const [submitted, setSubmitted] = useState(false);
async function handleRate(rating: "helpful" | "not_helpful") {
await onRate(slug, rating);
setSubmitted(true);
}
if (submitted) {
return (
<p className="text-sm text-muted-foreground text-center py-4">
Thanks for your feedback.
</p>
);
}
return (
<div className="flex items-center gap-3 justify-center py-4 border-t">
<span className="text-sm text-muted-foreground">Was this helpful?</span>
<button
onClick={() => handleRate("helpful")}
className="p-1.5 rounded hover:bg-green-50 dark:hover:bg-green-950 hover:text-green-600 transition-colors"
aria-label="Yes, helpful"
>
<ThumbsUp className="w-4 h-4" />
</button>
<button
onClick={() => handleRate("not_helpful")}
className="p-1.5 rounded hover:bg-red-50 dark:hover:bg-red-950 hover:text-red-600 transition-colors"
aria-label="Not helpful"
>
<ThumbsDown className="w-4 h-4" />
</button>
</div>
);
}The onRate prop keeps the component server-agnostic — the dashboard app passes an apiFetch-based handler, a future mobile app can pass its own.
Integration into HelpPage
// packages/ui/src/HelpPage.tsx
// Add onRate optional prop — if omitted, rating footer is hidden
interface HelpPageProps {
data: HelpPageData;
inDrawer?: boolean;
onRate?: (slug: string, rating: "helpful" | "not_helpful") => Promise<void>;
}
// At the bottom of the page render, after all sections:
{onRate && <HelpRating slug={data.slug} onRate={onRate} />}In the dashboard, pass onRate from a client wrapper:
// apps/dashboard/src/app/(dashboard)/help/[slug]/page.tsx
// becomes a client component wrapper that provides onRateAdmin Visibility (future)
The HelpPageRating table is queryable from manage portal analytics. A simple aggregate query gives the satisfaction rate per topic:
SELECT slug,
COUNT(*) FILTER (WHERE rating = 'helpful') AS helpful_count,
COUNT(*) FILTER (WHERE rating = 'not_helpful') AS not_helpful_count
FROM help_page_rating
GROUP BY slug
ORDER BY not_helpful_count DESC;No UI needed immediately — the data just needs to be collected now so it can be surfaced later.
Affected Files
| File | Change |
|---|---|
packages/db/prisma/schema.prisma | Add HelpPageRating model + HelpRating enum |
packages/ui/src/HelpRating.tsx | New component |
packages/ui/src/HelpPage.tsx | Add onRate prop; render <HelpRating> at bottom |
packages/ui/src/index.ts | Export HelpRating |
apps/api/src/routers/help.ts | New router — POST /tenant/v1/help/rating |
apps/api/src/app.ts | Register help router |
apps/api/src/index.ts | Register help router |
apps/dashboard/src/app/(dashboard)/help/[slug]/HelpPageClient.tsx | New — client wrapper providing onRate via apiFetch |
apps/dashboard/src/app/(dashboard)/help/[slug]/page.tsx | Render <HelpPageClient> |