@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 +1 -1
- package/template/.env.example +6 -0
- package/template/CHANGELOG.md +7 -0
- package/template/__tests__/chatbot-flag-gate.test.ts +25 -0
- package/template/__tests__/chatbot-flag-wiring.test.ts +45 -0
- package/template/app/p1/[[...p1]]/editor-client.tsx +25 -3
- package/template/components/ChatbotFlagProvider.tsx +49 -0
- package/template/constants/assets.ts +3 -0
- package/template/lib/chatbot-flag/feature-gate.ts +17 -0
- package/template/package.json +5 -2
- package/template/public/images/p1_logo.svg +5 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -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
|
package/template/CHANGELOG.md
CHANGED
|
@@ -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
|
-
<
|
|
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
|
|
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,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
|
+
}
|
package/template/package.json
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@pantheon-systems/cpub-react-sdk": "^5.2.1",
|
|
14
|
-
"@pantheon-systems/p1-
|
|
15
|
-
"@pantheon-systems/
|
|
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>
|