@pantheon-systems/create-p1-starter-kit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantheon-systems/create-p1-starter-kit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a new P1 starter project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,3 +19,9 @@ CSS_API_KEY=your-api-key
19
19
  # Show the RoleSwitcher dropdown in the P1 editor for local testing of
20
20
  # admin/editor/junior-editor permissions. Never enable this in production.
21
21
  # NEXT_PUBLIC_ENABLE_ROLE_SWITCHER=true
22
+
23
+ # --- AI chatbot (optional) ---
24
+ # LaunchDarkly client-side ID used to evaluate the `p1-chatbot` flag. It is
25
+ # public by design (safe to expose in the browser). When unset, the chatbot
26
+ # stays hidden.
27
+ # NEXT_PUBLIC_LD_CLIENT_ID=your-launchdarkly-client-side-id
@@ -1,5 +1,12 @@
1
1
  # @pantheon-systems/p1-starter
2
2
 
3
+ ## 1.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - @pantheon-systems/puck-css@0.6.0
8
+ - @pantheon-systems/p1-next-sdk@0.6.0
9
+
3
10
  ## 1.0.4
4
11
 
5
12
  ### Patch Changes
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../lib/chatbot-flag/feature-gate";
3
+
4
+ describe("shouldShowChatbot", () => {
5
+ it("is off when the flag is disabled, even with an agent URL", () => {
6
+ expect(shouldShowChatbot(false, "https://agent.example")).toBe(false);
7
+ });
8
+
9
+ it("is off when the agent URL is missing, even with the flag enabled", () => {
10
+ expect(shouldShowChatbot(true, undefined)).toBe(false);
11
+ expect(shouldShowChatbot(true, "")).toBe(false);
12
+ });
13
+
14
+ it("is on only when the flag is enabled AND the agent URL is set", () => {
15
+ expect(shouldShowChatbot(true, "https://agent.example")).toBe(true);
16
+ });
17
+
18
+ it("defaults off when the flag value is undefined (LD not yet resolved / offline)", () => {
19
+ expect(shouldShowChatbot(undefined, "https://agent.example")).toBe(false);
20
+ });
21
+
22
+ it("exposes the p1-chatbot flag key", () => {
23
+ expect(CHATBOT_FLAG_KEY).toBe("p1-chatbot");
24
+ });
25
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const appDir = resolve(__dirname, "..");
8
+
9
+ describe("editor-client gates the chatbot behind the p1-chatbot flag", () => {
10
+ const content = readFileSync(
11
+ resolve(appDir, "app/p1/[[...p1]]/editor-client.tsx"),
12
+ "utf-8",
13
+ );
14
+
15
+ it("reads LaunchDarkly flags via useFlags", () => {
16
+ expect(content).toContain("useFlags");
17
+ });
18
+
19
+ it("gates the AI plugin through shouldShowChatbot", () => {
20
+ expect(content).toContain("shouldShowChatbot");
21
+ });
22
+
23
+ it("wraps the editor in the chatbot flag provider", () => {
24
+ expect(content).toContain("ChatbotFlagProvider");
25
+ });
26
+
27
+ it("imports the plugin from the published @pantheon-systems/p1-ai-chat package", () => {
28
+ expect(content).toContain("@pantheon-systems/p1-ai-chat");
29
+ });
30
+ });
31
+
32
+ describe("chatbot flag provider is a client-side LaunchDarkly gate", () => {
33
+ const content = readFileSync(
34
+ resolve(appDir, "components/ChatbotFlagProvider.tsx"),
35
+ "utf-8",
36
+ );
37
+
38
+ it("initializes from the public client-side ID env var", () => {
39
+ expect(content).toContain("NEXT_PUBLIC_LD_CLIENT_ID");
40
+ });
41
+
42
+ it("uses the launchdarkly-react-client-sdk", () => {
43
+ expect(content).toContain("launchdarkly-react-client-sdk");
44
+ });
45
+ });
@@ -14,14 +14,19 @@ import {
14
14
  editorPathHref,
15
15
  } from "@pantheon-systems/puck-css";
16
16
  import { P1NextRouterProvider } from "@pantheon-systems/p1-next-sdk";
17
+ import { createAIChatPlugin } from "@pantheon-systems/p1-ai-chat";
18
+ import { useFlags } from "launchdarkly-react-client-sdk";
17
19
  import type { Checkpoint } from "@pantheon-systems/puck-css";
18
20
  import type { ContentRole } from "@pantheon-systems/puck-css";
21
+ import { P1_ASSETS } from "../../../constants/assets";
19
22
 
20
23
  import "@pantheon-systems/puck-css/styles.css";
21
24
  import "@pantheon-systems/puck-css/pds/styles.css";
22
25
 
26
+ import { ChatbotFlagProvider } from "../../../components/ChatbotFlagProvider";
23
27
  import { P1Lockup } from "../../../components/p1-lockup";
24
28
  import config from "../../../puck.config";
29
+ import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../lib/chatbot-flag/feature-gate";
25
30
 
26
31
  const DEFAULT_PAGE_DATA = {
27
32
  root: { props: { title: "New page" } },
@@ -128,7 +133,9 @@ export function EditorClientWrapper({ path }: { path: string }) {
128
133
  config={{ ...p1Config, userRole }}
129
134
  loginFallback={<P1SignInPage />}
130
135
  >
131
- <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
136
+ <ChatbotFlagProvider>
137
+ <EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
138
+ </ChatbotFlagProvider>
132
139
  </P1App>
133
140
  {process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
134
141
  <RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
@@ -198,6 +205,20 @@ function EditorContent({
198
205
  const router = useRouter();
199
206
  const { getToken } = useP1Auth();
200
207
  const p1Plugins = useP1Plugins(path, config);
208
+ const flags = useFlags();
209
+ const agentUrl = process.env.NEXT_PUBLIC_AGENT_URL;
210
+ const chatbotEnabled = shouldShowChatbot(flags[CHATBOT_FLAG_KEY], agentUrl);
211
+ const aiPlugin = React.useMemo(
212
+ () =>
213
+ chatbotEnabled && agentUrl
214
+ ? createAIChatPlugin({ agentUrl })
215
+ : null,
216
+ [chatbotEnabled, agentUrl],
217
+ );
218
+ const additionalPlugins = React.useMemo(
219
+ () => (aiPlugin ? [...p1Plugins, aiPlugin] : p1Plugins),
220
+ [p1Plugins, aiPlugin],
221
+ ) as typeof p1Plugins;
201
222
 
202
223
  const [redirecting, setRedirecting] = React.useState(false);
203
224
 
@@ -236,13 +257,14 @@ function EditorContent({
236
257
  const { loading, error, puckKey, puckProps } = useP1Editor({
237
258
  documentPath: path,
238
259
  puckConfig: editorConfig,
239
- additionalPlugins: p1Plugins,
260
+ additionalPlugins,
240
261
  onDocumentNotFound: handleDocumentNotFound,
241
262
  pluginOptions: {
242
263
  onDocumentSelect: handleDocumentSelect,
243
264
  selectedDocumentPath: path,
244
265
  siteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
245
266
  dashboardUrl: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL,
267
+ logoUrl: P1_ASSETS.LOGO_URL,
246
268
  },
247
269
  overrideOptions: {
248
270
  showDefaultPublish: false,
@@ -337,7 +359,7 @@ function EditorContent({
337
359
  </div>
338
360
  )}
339
361
  {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
340
- <Puck key={displayState.puckKey} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
362
+ <Puck key={`${displayState.puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
341
363
  </div>
342
364
  );
343
365
  }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { LDProvider } from "launchdarkly-react-client-sdk";
5
+ import { useP1Auth } from "@pantheon-systems/puck-css";
6
+
7
+ /**
8
+ * Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
9
+ * flag can be evaluated at runtime. The client-side ID is public by design.
10
+ *
11
+ * When NEXT_PUBLIC_LD_CLIENT_ID is unset (local dev / offline), LaunchDarkly is
12
+ * not initialized and children render without a provider — `useFlags()` then
13
+ * returns no flags, so the chatbot defaults to hidden.
14
+ */
15
+ export function ChatbotFlagProvider({
16
+ children,
17
+ }: {
18
+ children: React.ReactNode;
19
+ }) {
20
+ const clientSideID = process.env.NEXT_PUBLIC_LD_CLIENT_ID;
21
+ const { user } = useP1Auth();
22
+
23
+ if (!clientSideID) {
24
+ return <>{children}</>;
25
+ }
26
+
27
+ // LaunchDarkly evaluates the flag for the context present at mount and does not
28
+ // re-identify on context change. This provider mounts inside <P1App> (after
29
+ // auth), so the authenticated user is available here; the anonymous fallback
30
+ // only applies if it ever renders pre-auth.
31
+ //
32
+ // Key on the always-present, stable user id — email is optional on AuthUser, so
33
+ // keying on it would silently drop emailless users into the anonymous branch and
34
+ // lose per-user rollout stickiness. (LaunchDarkly also favors a non-PII key.)
35
+ // Email is kept as a targeting attribute.
36
+ const context = user
37
+ ? { kind: "user" as const, key: user.id, email: user.email }
38
+ : { kind: "user" as const, key: "anonymous", anonymous: true };
39
+
40
+ return (
41
+ <LDProvider
42
+ clientSideID={clientSideID}
43
+ context={context}
44
+ reactOptions={{ useCamelCaseFlagKeys: false }}
45
+ >
46
+ {children}
47
+ </LDProvider>
48
+ );
49
+ }
@@ -0,0 +1,3 @@
1
+ export const P1_ASSETS = {
2
+ LOGO_URL: "/images/p1_logo.svg",
3
+ } as const;
@@ -0,0 +1,17 @@
1
+ /** LaunchDarkly flag that gates the AI chatbot. Short-lived — removed once the
2
+ * chatbot ships to everyone in the alpha. */
3
+ export const CHATBOT_FLAG_KEY = "p1-chatbot";
4
+
5
+ /**
6
+ * Whether the AI chatbot plugin should be mounted in the editor.
7
+ *
8
+ * Requires both the `p1-chatbot` LaunchDarkly flag to be enabled and an agent
9
+ * URL to be configured. Defaults off when the flag is `undefined` (LD not yet
10
+ * resolved, unset client ID, or offline), so the chatbot stays hidden by default.
11
+ */
12
+ export function shouldShowChatbot(
13
+ flagEnabled: boolean | undefined,
14
+ agentUrl: string | undefined,
15
+ ): boolean {
16
+ return Boolean(flagEnabled && agentUrl);
17
+ }
@@ -11,11 +11,14 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@pantheon-systems/cpub-react-sdk": "^5.2.1",
14
- "@pantheon-systems/p1-next-sdk": "^0.5.0",
15
- "@pantheon-systems/puck-css": "^0.5.0",
14
+ "@pantheon-systems/p1-ai-chat": "^0.1.0",
15
+ "@pantheon-systems/p1-next-sdk": "^0.6.0",
16
+ "@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.12",
17
+ "@pantheon-systems/puck-css": "^0.6.0",
16
18
  "@puckeditor/core": "^0.21.1",
17
19
  "@tailwindcss/postcss": "^4.2.2",
18
20
  "classnames": "^2.5.1",
21
+ "launchdarkly-react-client-sdk": "^3.9.2",
19
22
  "next": "^16.2.6",
20
23
  "postcss": "^8.5.12",
21
24
  "react": "^19.2.5",
@@ -0,0 +1,5 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0 3C0 1.34315 1.34315 0 3 0H29C30.6569 0 32 1.34315 32 3V29C32 30.6569 30.6569 32 29 32H3C1.34315 32 0 30.6569 0 29V3Z" fill="#171717"/>
3
+ <path d="M6 7H16.9111V8.82232H18.7335V17.9112H16.9111V19.7335H9.64464V25.189H6.01139V7H6ZM15.0888 10.6333H9.63325V16.0888H15.0888V10.6333Z" fill="#FFDC28"/>
4
+ <path d="M20.5449 7H26.0005V25.1777H22.3672V10.6333H20.5449V7Z" fill="#FFDC28"/>
5
+ </svg>