Feedback Widget
A centralized feedback system built into the @anglinai/ui CorporateFooter component. Users on any AnglinAI project can submit feedback, which is analyzed by Claude for sentiment and category, turned into a HelpDesk ticket, and confirmed via email.
Architecture
βββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ Any AnglinAI Frontend β β feedback-api (CF Worker) ββ βPOST β ββ CorporateFooter ββββββΆβ 1. Validate input (Zod) ββ ββ FeedbackWidget β β 2. Claude Sonnet β analyze ββ (auth-aware) βββββββ 3. HelpDesk MCP β ticket ββ β JSONβ 4. Resend β confirm email ββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββ- User clicks Send Feedback in the footer
- A compact dialog appears with a comment field (and email field if not logged in)
- On submit, the widget POSTs to the
feedback-apiCloudflare Worker - The Worker runs Claude Sonnet to classify sentiment, category, and priority
- A HelpDesk ticket is created via MCP JSON-RPC with the analysis metadata
- If the user provided an email, a confirmation email is sent via Resend (non-blocking)
Adding the Feedback Widget to an App
Step 1: Install @anglinai/ui v0.3.0+
npm install @anglinai/ui@^0.3.0Ensure your .npmrc has:
@anglinai:registry=https://npm.pkg.github.comStep 2: Set the environment variable
Add to .env.local:
NEXT_PUBLIC_FEEDBACK_API_URL=https://feedback-api.anglin.comAdd to .env.local.example for documentation:
# βββ Feedback βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββNEXT_PUBLIC_FEEDBACK_API_URL=https://feedback-api.anglin.comStep 3: Create an AppFooter component
Create a client component that reads the auth context and passes the user info to the footer:
'use client';
import { CorporateFooter, type FeedbackUser } from '@anglinai/ui';import { useAuth } from '@/lib/auth-context';
const FEEDBACK_API_URL = process.env.NEXT_PUBLIC_FEEDBACK_API_URL || '';
export function AppFooter() { const { user } = useAuth();
const feedbackUser: FeedbackUser | undefined = user?.email ? { name: user.name, email: user.email } : undefined;
return ( <CorporateFooter feedbackEndpoint={FEEDBACK_API_URL ? `${FEEDBACK_API_URL}/api/feedback` : undefined} feedbackProjectName="My Project Name" feedbackUser={feedbackUser} copyrightHolder="TheAccessibleOrg" docsHref="https://help.myproject.com" /> );}Step 4: Add to the root layout
Place <AppFooter /> inside your AuthProvider so it has access to the user context. Make the body a flex column so the footer sticks to the bottom:
import { AppFooter } from '@/components/layout/AppFooter';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <body className="flex min-h-screen flex-col"> <ThemeProvider defaultTheme="system"> <AuthProvider> <main id="main-content" className="flex-1"> {children} </main> <AppFooter /> </AuthProvider> </ThemeProvider> </body> </html> );}Key points:
flex min-h-screen flex-colon<body>ensures the footer sticks to the bottomflex-1on<main>makes the content area fill available space<AppFooter />must be inside<AuthProvider>souseAuth()works- The
CorporateFooterhasmt-autobuilt in, so it naturally sits at the bottom
Step 5: Remove any other footer instances
If you previously had a CorporateFooter in a marketing layout or a custom footer component, remove it β the root layout footer now appears on every page.
CorporateFooter Props
| Prop | Type | Default | Description |
|---|---|---|---|
feedbackEndpoint | string | β | URL for the feedback API. Widget only renders when set. |
feedbackProjectName | string | β | Identifies the source project in tickets. Falls back to page hostname. |
feedbackUser | FeedbackUser | β | Pre-fills email and shows βSubmitting as {name}β. See below. |
copyrightHolder | string | "AnglinAI" | Name shown in the copyright line. Use "TheAccessibleOrg" for TAO products. |
docsHref | string | β | When set, appends a βDocumentationβ link to the footer nav. |
productName | string | β | When set, shows βPart of the AnglinAI familyβ text. |
links | FooterLink[] | Default set | Override the footer navigation links entirely. |
showVersion | boolean | true | Show/hide the VersionInfo component. |
versionProps | VersionInfoProps | β | Build version data for the VersionInfo modal. |
children | ReactNode | β | Project-specific content rendered above the corporate section. |
className | string | β | Additional CSS classes on the <footer> element. |
FeedbackUser Interface
interface FeedbackUser { name?: string; // Displayed as "Submitting as {name}" email: string; // Pre-filled, hidden from the form}When feedbackUser is provided:
- The email input is hidden (email is sent automatically)
- A line reading βSubmitting as {name}β appears below the comment field (with
aria-live="polite") - The
userNamefield is included in the POST payload
When feedbackUser is omitted:
- The email input is shown as optional (same as before v0.3.0)
- No
userNameis sent
Using the FeedbackWidget Standalone
The widget is also exported on its own if you want to place it somewhere other than the footer:
import { FeedbackWidget } from '@anglinai/ui';
<FeedbackWidget feedbackEndpoint="https://feedback-api.anglin.com/api/feedback" projectName="my-project"/>Feedback API
Base URL: https://feedback-api.anglin.com
Source: anglinai-monorepo/apps/feedback-api/
GET /health
Returns service health status.
{ "status": "ok", "version": "1.0.0", "uptime": 12345 }POST /api/feedback
Submit feedback for analysis and ticket creation.
Request body:
{ "pageUrl": "https://orgchart.anglin.com/dashboard", "comment": "The export button doesn't work on mobile", "userName": "Jane Doe", "project": "Accessible Org Chart Generator", "userAgent": "Mozilla/5.0 ..."}| Field | Type | Required | Description |
|---|---|---|---|
pageUrl | string | Yes | Full URL of the page (auto-captured by widget) |
comment | string | Yes | Feedback text (1β5000 chars) |
email | string | No | Userβs email for follow-up and confirmation |
userName | string | No | Userβs display name (sent when logged in) |
project | string | No | Project identifier for ticket tagging |
userAgent | string | No | Browser user agent (auto-captured by widget) |
Success response (201):
{ "data": { "message": "Feedback submitted successfully", "ticketId": "abc123" }, "error": null}Error response (400):
{ "data": null, "error": { "code": "VALIDATION_ERROR", "message": "Comment is required" }}Processing Pipeline
-
Validation β Zod schema checks all fields, returns 400 on failure
-
Claude Analysis β Sends the comment to Claude Sonnet which returns:
sentiment:{ label: "positive"|"negative"|"neutral", intensity: 0.0β1.0 }category:bug_report,feature_request,complaint,praise,question, orgeneraltitle: Prefixed ticket title (e.g.[Bug] Export fails on mobile)summary: 1β2 sentence description for the ticket
-
Priority Mapping:
Condition Priority Negative + bug + intensity β₯ 0.7 Urgent Negative + complaint + intensity β₯ 0.7 High Bug report Medium Feature request Medium Question Low Praise Background Everything else Low -
HelpDesk Ticket β Created via MCP JSON-RPC in the
theaccessibleorgtenant -
Confirmation Email β Sent via Resend with
reply-to: [email protected](non-blocking viawaitUntil)
CORS
- Production: Only allows origins listed in the
ALLOWED_ORIGINSsecret (comma-separated) - Development: Also allows
http://localhost:*
Secrets
Set via wrangler secret put:
| Secret | Description |
|---|---|
ANTHROPIC_API_KEY | Claude API key for feedback analysis |
RESEND_API_KEY | Resend API key for confirmation emails |
HELPDESK_API_KEY | HelpDesk MCP API key for ticket creation |
HELPDESK_TENANT_ID | HelpDesk tenant ID (theaccessibleorg) |
ALLOWED_ORIGINS | Comma-separated list of allowed CORS origins |
Cost Tracking
Every Claude API call logs a structured JSON entry to stdout with:
{ "type": "cost_tracking", "operation": "claude_analysis", "model": "claude-sonnet-4-20250514", "inputTokens": 245, "outputTokens": 89, "estimatedCostUsd": 0.002070, "project": "Accessible Org Chart Generator", "timestamp": "2026-03-03T12:00:00.000Z"}HelpDesk Tenant
Feedback tickets are created in the TheAccessibleOrg HelpDesk tenant:
- Slug:
theaccessibleorg - Owner: [email protected]
- Settings:
allowPublicSubmission: true,enableGuestAccess: true
Tickets include the AI-generated title, description with sentiment metadata, priority, contact info, and tags for the source project.
Email Notifications
When a user includes an email address, they receive a confirmation:
- From:
[email protected] - Reply-to:
[email protected] - Subject:
We received your feedback β Ticket #abc123 - Body: Greeting, ticket ID, βOur team will review it shortlyβ message
- Branding: White body, light gray wrapper, AnglinAI copyright footer
The email send is non-blocking β if Resend is down, the ticket is still created and the user still sees a success message.
Files
| File | Location | Purpose |
|---|---|---|
feedback-widget.tsx | anglinai-monorepo/packages/ui/src/components/ | React widget component |
corporate-footer.tsx | anglinai-monorepo/packages/ui/src/components/ | Footer integration |
index.ts | anglinai-monorepo/apps/feedback-api/src/ | Worker entry point |
routes/feedback.ts | anglinai-monorepo/apps/feedback-api/src/ | POST handler |
services/analyzer.ts | anglinai-monorepo/apps/feedback-api/src/ | Claude analysis |
services/helpdesk.ts | anglinai-monorepo/apps/feedback-api/src/ | HelpDesk ticket creation |
services/email.ts | anglinai-monorepo/apps/feedback-api/src/ | Resend confirmation |
AppFooter.tsx | accessible-org-chart/apps/web/src/components/layout/ | Org chart integration |
AppFooter.tsx | accessible-pdf-converter/apps/web/src/components/layout/ | PDF converter integration |
Legacy: Direct Supabase Endpoint
The original feedback system posted directly to a Supabase Edge Function at https://dktmitcptketnziahoho.supabase.co/functions/v1/feedback. This stored feedback in the public.feedback table without analysis or ticketing. The Edge Function and database table still exist and can be used as a fallback, but new integrations should use the feedback-api Worker described above.