@forwardpath/react

中文文档
0.3.0发布于 2026/11/03UNLICENSEDTypeScriptES Module近 30 天下载 4068

React provider, hook, chat and feedback widget for ForwardPath intake.

原始使用文档
文档目录14 节
  1. Install
  2. Quick start
  3. The publishable key is public
  4. Who the reporter is
  5. Verify users with userHash
  6. The email form
  7. Verified users from your server
  8. <ForwardPathProvider>
  9. useFeedback()
  10. <FeedbackWidget />
  11. <FeedbackChat />
  12. Attachments
  13. Screen capture
  14. Theming

@forwardpath/react

React components for ForwardPath intake. Wrap your app in <ForwardPathProvider>, then drop in <FeedbackWidget /> for a floating "Feedback" button, <FeedbackChat /> for an inline chat, or build your own UI with useFeedback(). Built on @forwardpath/js.

Install

npm install @forwardpath/react @ai-sdk/react ai

react and react-dom (v19), @ai-sdk/react (v4) and ai (v7) are peer dependencies. No Tailwind needed. Add @forwardpath/node on your server only to verify users with userHash or to mint sessions from your own route.

Quick start

  1. In the ForwardPath portal, create a publishable key for your project. Add each origin your app runs on (e.g. https://app.acme.com, http://localhost:3000) and pick how to collect emails. Copy the key (fppk_…).

  2. Wrap your app in the provider and import the styles once.

// app/providers.tsx
"use client";

import { ForwardPathProvider } from "@forwardpath/react";
import "@forwardpath/react/styles.css";

type User = { id: string; email?: string | null; name?: string | null };

export function Providers({
  user,
  children,
}: {
  user: User | null;
  children: React.ReactNode;
}) {
  return (
    <ForwardPathProvider
      baseUrl="https://app.forwardpath.ai"
      publishableKey={process.env.NEXT_PUBLIC_FORWARDPATH_KEY!}
      user={user}
      appName="Acme"
      appVersion={process.env.NEXT_PUBLIC_APP_VERSION}
      environment={process.env.NODE_ENV}
    >
      {children}
    </ForwardPathProvider>
  );
}
  1. Add the widget to your layout, inside the provider.
// app/layout.tsx
import { FeedbackWidget } from "@forwardpath/react";

import { getUser } from "@/auth"; // your own auth
import { Providers } from "./providers";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser(); // null when signed out
  return (
    <html lang="en">
      <body>
        <Providers user={user}>
          {children}
          <FeedbackWidget />
        </Providers>
      </body>
    </html>
  );
}

Or render the chat inline on a page of its own:

// app/support/page.tsx
"use client";

import { FeedbackChat } from "@forwardpath/react";

export default function SupportPage() {
  return (
    <FeedbackChat
      placeholder="What went wrong?"
      onSubmitted={(report) => console.log("Filed", report.customerReference)}
    />
  );
}

The publishable key is public

The publishable key is public by design. Anyone can read it from your page, so it can only start intake chats for its one project, and only from the origins allowed on the key. ForwardPath rate-limits it. Never put your secret API key (fp_…) or the key's identity secret (fpiv_…) in browser code.

Who the reporter is

You passReporterMeaning
user and userHashverifiedYour server signed the user.
user onlyclaimedThe browser named the user. Anyone can send any id or email, so treat it as unverified.
neitheranonymousA visitor with no account.

For an anonymous visitor the provider keeps a random id in localStorage (forwardpath:anonymous-id), so repeat visits map to the same reporter. Where storage is blocked, the id lasts for the page load.

When publishableKey, user.id, user.email or userHash changes, the provider drops the cached session and mints a new one, so sign-in and sign-out need no page load. A new user object with the same fields keeps the session.

Verify users with userHash

Compute the hash on your server with createUserHash from @forwardpath/node and the key's identity secret (fpiv_…, shown once when you create the key). Pass it to the provider next to user.

// app/layout.tsx (a server component)
import { createUserHash } from "@forwardpath/node";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();
  const userHash = user
    ? createUserHash({
        identitySecret: process.env.FORWARDPATH_IDENTITY_SECRET!,
        user,
      })
    : undefined;
  // Pass both on: <ForwardPathProvider user={user} userHash={userHash} …>
  return (
    <html lang="en">
      <body>
        <Providers user={user} userHash={userHash}>
          {children}
          <FeedbackWidget />
        </Providers>
      </body>
    </html>
  );
}

The hash signs user.id and user.email, not the name. Hash the same values you pass as user: a mismatch fails the session with a 401. Without userHash the user is claimed.

The email form

When the agent has no email for the reporter, e.g. an anonymous visitor or a user without email, it shows a short form in the chat: Email, an optional Name and Send. Once the user answers, the chat carries on by itself. While the form is open, the composer is disabled.

Set contactCollection per key in the portal:

  • required: the agent files the report only after the user gives an email.
  • optional: the form also shows Skip.
  • off: the agent never asks.

The server saves the submitted email on the external reporter before the assistant continues. Filing links that reporter to the request and includes their contact details in the request metadata and Linear description. A typed email is marked as self-reported; it does not create a portal account.

If your app already knows the signed-in user's email, pass it in user={{ id: user.id, email: user.email, name: user.name }} on ForwardPathProvider. The chat uses it without asking again.

Verified users from your server

To mint sessions on your server with your secret API key instead, leave out publishableKey and add a session route with @forwardpath/node. The provider POSTs { clientContext } to /api/forwardpath/session by default and expects { token, expiresAt } back. These reporters are always verified.

// app/api/forwardpath/session/route.ts
import {
  createForwardPathClient,
  createIntakeSessionHandler,
} from "@forwardpath/node";

import { auth } from "@/auth";

const client = createForwardPathClient({
  apiKey: process.env.FORWARDPATH_API_KEY!,
  baseUrl: "https://app.forwardpath.ai",
});

export const POST = createIntakeSessionHandler({
  client,
  projectSlug: "acme-web",
  // Return the signed-in user, or null to answer 401.
  getUser: async () => {
    const session = await auth();
    if (!session?.user) return null;
    return {
      externalId: session.user.id,
      email: session.user.email,
      name: session.user.name ?? undefined,
    };
  },
});
<ForwardPathProvider baseUrl="https://app.forwardpath.ai" sessionKey={userId}>

Your secret API key can mint sessions for any user. Keep it on your server. The provider only ever sends the short-lived session token to ForwardPath.

<ForwardPathProvider>

PropDescription
baseUrlFPOS origin, e.g. https://app.forwardpath.ai.
publishableKeyPublishable key (fppk_…). The browser mints sessions with it.
user{ id, email?, name? } for the signed-in user, or null for an anonymous visitor. Used with publishableKey.
userHashcreateUserHash() output from your server. Makes user verified.
sessionEndpointYour route that mints a session. Default /api/forwardpath/session.
getSessionTokenMint the session yourself. Gets { clientContext }; returns { token, expiresAt } or a token string.
sessionKeyWho the session is for, e.g. the signed-in user's id. Changing it drops the cached session.
appName, appVersion, environment, customSent with each message so the team sees where feedback came from.
fetchCustom fetch for every request.

getSessionToken wins over publishableKey, which wins over sessionEndpoint.

The provider mints a token on first use and keeps it in memory. It mints a new one 60 seconds before expiresAt, and once more if the API answers 401. A bare string from getSessionToken is kept until a 401. Pass sessionKey so a user switch without a page load doesn't reuse the old user's session.

useFeedback()

Returns { messages, sendMessage, status, error, stop, retry, reset, submitted, attachments, uploading, attachmentError, attach, removeAttachment, contactRequest, submitContact }.

  • sendMessage(text) sends a user message with any pending attachments. It does nothing while contactRequest is open.
  • status is the AI SDK chat status: ready, submitted, streaming or error.
  • retry() resends the last message after an error.
  • reset() clears the chat.
  • submitted is the latest successful submission ({ customerReference, title, ... }), or null.
  • attach(file) uploads a File to send with the next message. See Attachments.
  • attachments lists the uploaded files waiting for the next message, and removeAttachment(id) drops one.
  • uploading counts uploads in flight; attachmentError says why the last one failed.
  • contactRequest is the agent's open request for an email ({ toolCallId, canSkip }), or null. See The email form.
  • submitContact(toolCallId, { email, name? }) answers it, or submitContact(toolCallId, { skipped: true }) when canSkip is true. It trims values, drops an empty name and rejects an invalid email. The chat then carries on by itself. <FeedbackChat> renders this form for you.

Each useFeedback() call runs its own conversation. To drive the chat that <FeedbackChat> or <FeedbackWidget> shows, pass your result as their feedback prop.

import { useFeedback } from "@forwardpath/react";

function MyChat() {
  const { messages, sendMessage, submitted } = useFeedback();
  if (submitted) return <p>Thanks! Your reference is {submitted.customerReference}.</p>;
  // ...
}

<FeedbackWidget />

A fixed launcher button that opens <FeedbackChat /> in a panel. Render it once, inside the provider.

PropDescription
positionbottom-right (default) or bottom-left.
labelLauncher text and panel title. Default Feedback.
titleOverride the panel title independently of the launcher.
launcherIconReact node for the launcher mark. Defaults to assistantIcon, then a built-in chat mark. Pass null to hide.
defaultOpenOpen the panel on mount.
classNameAdded to the widget's root element.
styleInline styles and typed --fp-* theme variables on the widget root.

It also takes every <FeedbackChat /> prop. className and style apply to the widget root so the theme is inherited by the panel. Esc or the close button closes the panel and returns focus to the launcher. Closing hides the panel without ending the conversation.

<FeedbackChat />

PropDescription
placeholderComposer placeholder.
assistantNameDisplay name on assistant messages and in the widget header. Default ForwardPath assistant. Does not change the model or its instructions.
assistantIconDecorative React node, such as an SVG or an image with alt="". Used in the welcome state, assistant messages and widget header. Pass null to hide.
welcomeTitleHeading before the first message. Default How can we help?.
welcomeMessageSupporting text before the first message. Does not send a message or start a session.
onSubmitted(report)Called once per successful submission.
classNameAdded to the root element.
styleInline styles and typed --fp-* theme variables.
feedbackA useFeedback() result to render instead of the chat's own.

Messages render as plain text with line breaks. HTML in a message shows as text and never runs. When the agent asks for an email, the form shows inside its message.

Attachments

Users can attach files with the paperclip button or by dropping them on the chat. Each file uploads at once and shows as a chip above the composer until the next message sends it. Image attachments are shown to the agent, and all attachments can be added to the filed request.

Before uploading, the chat checks that each file:

  • is under 10 MB (the upload request, framing included, must fit in 10 MB),
  • is a PNG, JPEG, WebP or GIF image, PDF, plain text, CSV or JSON file (or has no type, which uploads as application/octet-stream), and
  • doesn't take the conversation past 10 attachments.

To attach a file from your own code, e.g. a log export, call attach:

"use client";

import { FeedbackWidget, useFeedback } from "@forwardpath/react";

export function Feedback({ getLogs }: { getLogs: () => string }) {
  const feedback = useFeedback();
  async function attachLogs() {
    await feedback.attach(
      new File([getLogs()], "app-log.txt", { type: "text/plain" }),
    );
  }
  return (
    <>
      <button onClick={attachLogs}>Attach logs</button>
      <FeedbackWidget feedback={feedback} />
    </>
  );
}

attach rejects if a check or the upload fails, and sets attachmentError.

Screen capture

The "Capture screen" button asks the browser to share a screen, window or tab with getDisplayMedia, saves one frame as screenshot-<ISO time>.png, stops sharing and attaches the image. Nothing else is recorded.

  • The browser prompts the user each time. Cancelling the prompt does nothing.
  • The button only shows where navigator.mediaDevices.getDisplayMedia exists: secure contexts (HTTPS or localhost) in desktop browsers. Most mobile browsers don't support it.
  • In an iframe, the parent page must allow it: <iframe allow="display-capture">. A Permissions-Policy header that blocks display-capture disables it too.

Theming

Brand each implementation without replacing the chat UI:

<FeedbackWidget
  label="Help"
  title="Acme support"
  assistantName="Acme assistant"
  assistantIcon={<img src="/acme-mark.svg" alt="" />}
  welcomeTitle="How can we help?"
  welcomeMessage="Tell us what's happening or what you'd like to improve."
  style={{
    "--fp-accent": "#0f766e",
    "--fp-accent-foreground": "#ffffff",
    "--fp-radius": "16px",
    "--fp-panel-width": "420px",
  }}
/>

The launcher uses the same mark unless you supply launcherIcon. All icons are decorative; label, title and assistantName provide the accessible text. Choose an accent and foreground pair with readable contrast in your app's light and dark themes. FeedbackStyle is exported for sharing typed styles between widget and inline chat instances.

Every class starts with fp-. Set these variables on :root, an ancestor or .fp-root:

:root {
  --fp-accent: #0f766e;
  --fp-accent-foreground: #ffffff;
  --fp-bg: #ffffff;
  --fp-fg: #111827;
  --fp-radius: 8px;
  --fp-font: "Inter", sans-serif;
  --fp-z-index: 1000; /* widget stacking order */
  --fp-panel-width: 400px;
  --fp-panel-height: 620px;
}

Add data-fp-theme="dark" to an ancestor (e.g. <html>) for dark defaults. If you set --fp-bg or --fp-fg yourself, also set them under your dark-mode selector.

版本与发布记录

@forwardpath/react 共发布 5 个版本,首次发布于 2026/10/22;过去 12 个月发布了 5 个版本。

版本发布日期距今
0.3.0latest2026/11/0313 天前
0.2.02026/10/2324 天前
0.1.12026/10/2224 天前
0.1.02026/10/2224 天前
0.0.0-stage2026/10/2224 天前

关于 @forwardpath/react 的常见问题

@forwardpath/react 是什么?

React provider, hook, chat and feedback widget for ForwardPath intake.

如何安装 @forwardpath/react?

在项目目录运行 npm install @forwardpath/react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:ai(^7)、react(^19)、react-dom(^19)、@ai-sdk/react(^4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@forwardpath/react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@forwardpath/react 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@forwardpath/react 可以商用吗?

@forwardpath/react 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@forwardpath/react 的下载量有多少?

根据 npm 官方下载统计,@forwardpath/react 在 2026/10/16 – 2026/11/14 这 30 天内被下载 4,068 次,日均约 136 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@forwardpath/react 还在维护吗?

最新版本 0.3.0 发布于 2026/11/03(13 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/10/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。