Skip to main content

AI assists — the reusable service & front-end kit

The product-wide AI "assists" are small, user-initiated helpers. They all go through one backend service and one front-end kit — reuse them; do not hand-roll a one-off AI button, drawer, or route.

Backend — AiAssistService

api/src/domains/ai-assist.tsAiAssistService(db, ai), exposed by api/src/routes/ai.ts under /v1/ai/*. Each method:

  • pulls the data it needs through the existing domain services (SurveyService.get, ResponseService.detail, VerbatimService.list, LeadService.detail, AlertService.list, ReportingService.agentScorecard) so survey/own-scope guards are inherited;
  • makes one ai.completeJson<T>({ messages, schema, responseSchema }) call (constrained JSON);
  • fails loudly — a missing entity throws NotFoundError (404), an AI failure throws AppError(..., 'ai_unavailable', 502), and empty data short-circuits without a model call.

The ten assists: improve-question, translate-survey, summarize-verbatims, suggest-topics, draft-reply, triage-alert, qualify-lead, coaching-plan, write-invite, report-narrative. Each route is gated on a sensible existing permission and the ai feature flag (system admins bypass).

Contrast with the Executive view, which is deterministic-first and degrades gracefully, and with domain AI (capture-time sentiment/coding, survey generation, feedback structuring, theme generation) which lives in its own domain service. All of it uses the one Gemini AiProvider port — no provider SDK appears outside the adapter.

Front end — the AiAssist kit

frontend/src/components/ai/AiAssist.tsx:

  • AiLauncher — a one-tag Sparkles button that runs a producer() and renders the result in a side-sheet Drawer via render(). The common case.
  • AiButton — the bare trigger; useAi<T>() — the loading/error/result hook for bespoke flows (e.g. the translate language picker, the inline "Write with AI" that fills a form).
  • CopyField (labelled read-only block + copy) and AiList for the result body.

AiButton/AiLauncher auto-hide when the ai feature flag is off for the user (system admins bypass), so every assist is gated in one place. The frontend api lives in frontend/src/lib/api/ai.ts.

Adding a new assist

  1. Add a method to AiAssistService (fetch via a domain service → one completeJson).
  2. Add the route to api/src/routes/ai.ts (permission gate; the featureGate('ai') already applies).
  3. Add the request schema to packages/contracts/src/ai.ts and the DTO to frontend/src/lib/api/types.ts + a method in frontend/src/lib/api/ai.ts.
  4. Surface it with <AiLauncher … /> at the call site.