@drawnagency/primitives 0.1.70 → 0.1.71

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.
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-TZQPOR5A.js";
10
10
  import {
11
11
  IndexSchema
12
- } from "./chunk-QKOIEWZ4.js";
12
+ } from "./chunk-YMVYTYGQ.js";
13
13
  import {
14
14
  getAllSchemas
15
15
  } from "./chunk-VKAGMEKE.js";
@@ -3,7 +3,7 @@ import {
3
3
  pluralize,
4
4
  stripHtmlToPlainText,
5
5
  truncate
6
- } from "./chunk-VVK6ZRCN.js";
6
+ } from "./chunk-RTYJZAKR.js";
7
7
  import {
8
8
  DeleteButton,
9
9
  DragHandle,
@@ -43,7 +43,7 @@ import {
43
43
  } from "./chunk-TZQPOR5A.js";
44
44
  import {
45
45
  SingleMediaReferenceSchema
46
- } from "./chunk-QKOIEWZ4.js";
46
+ } from "./chunk-YMVYTYGQ.js";
47
47
  import {
48
48
  defineSection,
49
49
  getSection
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HexColorSchema
3
- } from "./chunk-QKOIEWZ4.js";
3
+ } from "./chunk-YMVYTYGQ.js";
4
4
 
5
5
  // src/schemas/audience.ts
6
6
  import { z } from "zod";
@@ -30,7 +30,7 @@ import {
30
30
  import {
31
31
  HexColorSchema,
32
32
  SingleMediaReferenceSchema
33
- } from "./chunk-QKOIEWZ4.js";
33
+ } from "./chunk-YMVYTYGQ.js";
34
34
  import {
35
35
  defineSection
36
36
  } from "./chunk-VKAGMEKE.js";
@@ -208,8 +208,16 @@ var ChatbotTaskSchema = z3.object({
208
208
  var ChatbotConfigSchema = z3.object({
209
209
  enabled: z3.boolean().default(false),
210
210
  systemPrompt: z3.string().max(8e3).default(""),
211
- // Audience names the widget is shown to; [] = all audiences.
211
+ // Audience names the widget is shown to — strict opt-in: [] = no viewer
212
+ // sees it (editors always can via /edit). Gates: [...slug].astro + /api/chat.
212
213
  audiences: z3.array(z3.string()).default([]),
214
+ // Per-audience prompt overlays, keyed by audience name. Appended AFTER the
215
+ // base systemPrompt, and ONLY for requests whose session audience matches
216
+ // (or an editor's explicit test-panel simulation) — enforced in
217
+ // assembleBrandChat. Keys are not validated against the audience list:
218
+ // entries for deselected/renamed audiences are inert (strict opt-in means
219
+ // those audiences cannot chat at all).
220
+ audiencePrompts: z3.record(z3.string(), z3.string().max(8e3)).default({}),
213
221
  tasks: z3.array(ChatbotTaskSchema).max(20).default([])
214
222
  }).superRefine((cfg, ctx) => {
215
223
  const seen = /* @__PURE__ */ new Set();
@@ -238,7 +246,7 @@ var SiteConfigSchema = z3.object({
238
246
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
239
247
  // sites table, not here). Every field defaults so pre-existing
240
248
  // site-config.json files keep parsing.
241
- chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [] })
249
+ chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [], audiencePrompts: {} })
242
250
  });
243
251
 
244
252
  export {
@@ -12,6 +12,7 @@ export declare function streamChat(body: {
12
12
  sessionId: string;
13
13
  taskId?: string;
14
14
  draftChatbot?: unknown;
15
+ simulateAudience?: string;
15
16
  }, onText: (text: string) => void): Promise<{
16
17
  ok: true;
17
18
  truncated: boolean;
@@ -20,7 +21,7 @@ export declare function streamChat(body: {
20
21
  error: string;
21
22
  retryAfterMs?: number;
22
23
  }>;
23
- export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, variant }: {
24
+ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, simulateAudience, variant }: {
24
25
  open: boolean;
25
26
  onClose: () => void;
26
27
  siteName: string;
@@ -31,6 +32,8 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
31
32
  isEditor?: boolean;
32
33
  /** Set by the configurator's test panel and the /edit chat FAB. */
33
34
  draftChatbot?: unknown;
35
+ /** Configurator test panel only: preview this audience's prompt overlay. */
36
+ simulateAudience?: string;
34
37
  /** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
35
38
  variant?: "viewer" | "editor";
36
39
  }): import("react/jsx-runtime").JSX.Element | null;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ChatPanel.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AA6B5E;;kEAEkE;AAClE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAcD,8EAA8E;AAC9E,wBAAsB,UAAU,CAC9B,IAAI,EAAE;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EAC7F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCjG;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAkB,EAAE,EAAE;IAChH,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mFAAmF;IACnF,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC/B,kDA6HA"}
1
+ {"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ChatPanel.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAoB5E;;kEAEkE;AAClE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAcD,8EAA8E;AAC9E,wBAAsB,UAAU,CAC9B,IAAI,EAAE;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,EACxH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCjG;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAkB,EAAE,EAAE;IAClI,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC/B,kDA8JA"}
@@ -23,7 +23,7 @@ import "../../chunk-4YEOTFVU.js";
23
23
  import {
24
24
  cn
25
25
  } from "../../chunk-7EOSDFS3.js";
26
- import "../../chunk-QKOIEWZ4.js";
26
+ import "../../chunk-YMVYTYGQ.js";
27
27
  import "../../chunk-VKAGMEKE.js";
28
28
  import "../../chunk-JSBRDJBE.js";
29
29
 
@@ -2,7 +2,7 @@ import {
2
2
  ColorItemSchema,
3
3
  ColorSpaceSchema,
4
4
  brandGuideSectionDefs
5
- } from "../../chunk-VVK6ZRCN.js";
5
+ } from "../../chunk-RTYJZAKR.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-KWBM27UR.js";
8
8
  import "../../chunk-I5737FJY.js";
@@ -11,7 +11,7 @@ import "../../chunk-3DCG4DNU.js";
11
11
  import "../../chunk-WDSVHKAH.js";
12
12
  import "../../chunk-4YEOTFVU.js";
13
13
  import "../../chunk-7EOSDFS3.js";
14
- import "../../chunk-QKOIEWZ4.js";
14
+ import "../../chunk-YMVYTYGQ.js";
15
15
  import "../../chunk-VKAGMEKE.js";
16
16
  import "../../chunk-JSBRDJBE.js";
17
17
  export {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-TYYCKVAW.js";
4
- import "../../chunk-VVK6ZRCN.js";
3
+ } from "../../chunk-MU3CDERQ.js";
4
+ import "../../chunk-RTYJZAKR.js";
5
5
  import "../../chunk-2BHPWO7M.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-WXVKX6PY.js";
@@ -15,7 +15,7 @@ import "../../chunk-53DKVAB6.js";
15
15
  import "../../chunk-4YEOTFVU.js";
16
16
  import "../../chunk-7EOSDFS3.js";
17
17
  import "../../chunk-TZQPOR5A.js";
18
- import "../../chunk-QKOIEWZ4.js";
18
+ import "../../chunk-YMVYTYGQ.js";
19
19
  import {
20
20
  registerRichText,
21
21
  registerSchema
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-TYYCKVAW.js";
4
- import "../../chunk-VVK6ZRCN.js";
3
+ } from "../../chunk-MU3CDERQ.js";
4
+ import "../../chunk-RTYJZAKR.js";
5
5
  import "../../chunk-2BHPWO7M.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-WXVKX6PY.js";
@@ -15,7 +15,7 @@ import "../../chunk-53DKVAB6.js";
15
15
  import "../../chunk-4YEOTFVU.js";
16
16
  import "../../chunk-7EOSDFS3.js";
17
17
  import "../../chunk-TZQPOR5A.js";
18
- import "../../chunk-QKOIEWZ4.js";
18
+ import "../../chunk-YMVYTYGQ.js";
19
19
  import {
20
20
  registerSection
21
21
  } from "../../chunk-VKAGMEKE.js";
@@ -1 +1 @@
1
- {"version":3,"file":"ChatbotConfigModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/ChatbotConfigModal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAA8B,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,UAAU,KAAK;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACjD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAsBD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,KAAK,2CAkHxH"}
1
+ {"version":3,"file":"ChatbotConfigModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/ChatbotConfigModal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAA8B,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGjD,UAAU,KAAK;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACjD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAsBD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,KAAK,2CA0JxH"}
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  AudienceNameSchema,
34
34
  MediaGridOptionsSchema,
35
35
  slugifyAudienceName
36
- } from "./chunk-EWKCEPMW.js";
36
+ } from "./chunk-NR7UB6DZ.js";
37
37
  import {
38
38
  AudienceSchema,
39
39
  RoleSchema,
@@ -55,7 +55,7 @@ import {
55
55
  loadStaticSiteContent,
56
56
  mergeSiteContent,
57
57
  safeRedirect
58
- } from "./chunk-WNPMBFD6.js";
58
+ } from "./chunk-GKV77AA6.js";
59
59
  import {
60
60
  isSameOriginRequest,
61
61
  requireSessionSecret,
@@ -107,7 +107,7 @@ import {
107
107
  TextLineSchema,
108
108
  VariantSchema,
109
109
  normalizeSiteIndex
110
- } from "./chunk-QKOIEWZ4.js";
110
+ } from "./chunk-YMVYTYGQ.js";
111
111
  import {
112
112
  clearRegistry,
113
113
  createRegistry,
package/dist/lib/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  loadStaticSiteContent,
8
8
  mergeSiteContent,
9
9
  safeRedirect
10
- } from "../chunk-WNPMBFD6.js";
10
+ } from "../chunk-GKV77AA6.js";
11
11
  import "../chunk-S2L3BPLS.js";
12
12
  import "../chunk-75P6HWPN.js";
13
13
  import {
@@ -33,7 +33,7 @@ import {
33
33
  cn
34
34
  } from "../chunk-7EOSDFS3.js";
35
35
  import "../chunk-TZQPOR5A.js";
36
- import "../chunk-QKOIEWZ4.js";
36
+ import "../chunk-YMVYTYGQ.js";
37
37
  import {
38
38
  clearRegistry,
39
39
  createRegistry,
@@ -20,7 +20,7 @@ import {
20
20
  MediaConfigSchema,
21
21
  MediaItemSchema,
22
22
  VariantSchema
23
- } from "../chunk-QKOIEWZ4.js";
23
+ } from "../chunk-YMVYTYGQ.js";
24
24
  import "../chunk-JSBRDJBE.js";
25
25
  export {
26
26
  EXT_TO_MIME,
@@ -3,7 +3,7 @@ import {
3
3
  AudienceNameSchema,
4
4
  MediaGridOptionsSchema,
5
5
  slugifyAudienceName
6
- } from "../chunk-EWKCEPMW.js";
6
+ } from "../chunk-NR7UB6DZ.js";
7
7
  import {
8
8
  AudienceSchema,
9
9
  RoleSchema,
@@ -39,7 +39,7 @@ import {
39
39
  TextLineSchema,
40
40
  VariantSchema,
41
41
  normalizeSiteIndex
42
- } from "../chunk-QKOIEWZ4.js";
42
+ } from "../chunk-YMVYTYGQ.js";
43
43
  import "../chunk-VKAGMEKE.js";
44
44
  import "../chunk-NSCT3AMV.js";
45
45
  import "../chunk-JSBRDJBE.js";
@@ -161,6 +161,7 @@ export declare const ChatbotConfigSchema: z.ZodObject<{
161
161
  enabled: z.ZodDefault<z.ZodBoolean>;
162
162
  systemPrompt: z.ZodDefault<z.ZodString>;
163
163
  audiences: z.ZodDefault<z.ZodArray<z.ZodString>>;
164
+ audiencePrompts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
164
165
  tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
165
166
  id: z.ZodString;
166
167
  label: z.ZodString;
@@ -194,6 +195,7 @@ export declare const SiteConfigSchema: z.ZodObject<{
194
195
  enabled: z.ZodDefault<z.ZodBoolean>;
195
196
  systemPrompt: z.ZodDefault<z.ZodString>;
196
197
  audiences: z.ZodDefault<z.ZodArray<z.ZodString>>;
198
+ audiencePrompts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
197
199
  tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
198
200
  id: z.ZodString;
199
201
  label: z.ZodString;
@@ -1 +1 @@
1
- {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/schemas/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAO5D,eAAO,MAAM,cAAc,0DAA2D,CAAC;AAEvF,eAAO,MAAM,gBAAgB;;;EAA+B,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;iBASrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA0C9C,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6CtB,CAAC;AAEL,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAyE,CAAC;AAElG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAE1D;AAMD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;iBAgB5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/schemas/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAO5D,eAAO,MAAM,cAAc,0DAA2D,CAAC;AAEvF,eAAO,MAAM,gBAAgB;;;EAA+B,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;iBASrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA0C9C,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6CtB,CAAC;AAEL,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAAyE,CAAC;AAElG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAE1D;AAMD,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,mBAAmB;;;;;;;;;;;iBAwB5B,CAAC;AACL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -1,21 +1,15 @@
1
- import { useEffect, useRef, useState, type MouseEvent } from "react";
1
+ import { useEffect, useRef, useState, type KeyboardEvent, type MouseEvent } from "react";
2
2
  import { marked } from "marked";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { ensureSanitizer, sanitizeHtml } from "../../lib/sanitize";
5
+ import { Select } from "../shared/Select";
6
+ import { IconButton } from "../shared/IconButton";
7
+ import { RemoveIcon } from "../shared/icons";
5
8
 
6
9
  export interface ChatMessage { role: "user" | "assistant"; content: string }
7
10
 
8
11
  const SESSION_KEY = "bp-chat-session";
9
12
 
10
- // Astro's trailingSlash: "ignore" means the viewer can be on "/brand/" while
11
- // grounding links say "/brand#voice-tone" — the pathnames are equivalent
12
- // but not literally equal. Per the HTML spec, same-document fragment
13
- // navigation requires URL equality *excluding* the fragment, so a literal
14
- // mismatch (e.g. "/brand/" vs "/brand") can't rely on native navigation —
15
- // this helper only detects that "equivalent but not equal" case so we can
16
- // handle it ourselves. "/" stays "/" rather than collapsing to "".
17
- const normalizePathname = (p: string): string => p.replace(/\/+$/, "") || "/";
18
-
19
13
  function sessionId(): string {
20
14
  let id = window.localStorage.getItem(SESSION_KEY);
21
15
  if (!id) {
@@ -44,7 +38,7 @@ export function assistantHtml(md: string): string {
44
38
  const MD_STYLES =
45
39
  "[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 " +
46
40
  "[&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 " +
47
- "[&_a]:underline [&_a]:underline-offset-2 " +
41
+ "[&_a]:underline [&_a]:underline-offset-2 [&_a]:text-primary " +
48
42
  "[&_code]:rounded [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] " +
49
43
  "[&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-base-200 [&_pre]:p-2 " +
50
44
  "[&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold " +
@@ -53,7 +47,7 @@ const MD_STYLES =
53
47
 
54
48
  /** Parse our /api/chat SSE frames out of a fetch body. Exported for tests. */
55
49
  export async function streamChat(
56
- body: { messages: ChatMessage[]; sessionId: string; taskId?: string; draftChatbot?: unknown },
50
+ body: { messages: ChatMessage[]; sessionId: string; taskId?: string; draftChatbot?: unknown; simulateAudience?: string },
57
51
  onText: (text: string) => void,
58
52
  ): Promise<{ ok: true; truncated: boolean } | { ok: false; error: string; retryAfterMs?: number }> {
59
53
  try {
@@ -98,7 +92,7 @@ export async function streamChat(
98
92
  }
99
93
  }
100
94
 
101
- export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, variant = "viewer" }: {
95
+ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, simulateAudience, variant = "viewer" }: {
102
96
  open: boolean;
103
97
  onClose: () => void;
104
98
  siteName: string;
@@ -106,6 +100,8 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
106
100
  isEditor?: boolean;
107
101
  /** Set by the configurator's test panel and the /edit chat FAB. */
108
102
  draftChatbot?: unknown;
103
+ /** Configurator test panel only: preview this audience's prompt overlay. */
104
+ simulateAudience?: string;
109
105
  /** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
110
106
  variant?: "viewer" | "editor";
111
107
  }) {
@@ -115,6 +111,33 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
115
111
  const [busy, setBusy] = useState(false);
116
112
  const [notice, setNotice] = useState<string | null>(null);
117
113
  const streamingRef = useRef("");
114
+ const composerRef = useRef<HTMLTextAreaElement>(null);
115
+
116
+ // Autosize the composer: height tracks content up to the className's
117
+ // 4-line max-h cap, then it scrolls internally. Keyed on `input` so the
118
+ // post-send clear snaps it back to a single row. (field-sizing: content
119
+ // would do this in CSS alone, but it's still Chromium-only.)
120
+ useEffect(() => {
121
+ const el = composerRef.current;
122
+ if (!el) return;
123
+ el.style.height = "auto";
124
+ el.style.height = `${el.scrollHeight}px`;
125
+ }, [input]);
126
+
127
+ function onComposerKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
128
+ // Chat convention: Enter sends, Shift+Enter inserts a newline.
129
+ if (e.key === "Enter" && !e.shiftKey) {
130
+ e.preventDefault();
131
+ void send();
132
+ }
133
+ }
134
+
135
+ // The tasks prop can shrink live (the configurator's Preview-as filters it):
136
+ // never keep a selected task that's no longer offered, or the next send would
137
+ // assemble a task the previewed audience can't access.
138
+ useEffect(() => {
139
+ if (taskId && !tasks.some((t) => t.id === taskId)) setTaskId("");
140
+ }, [taskId, tasks]);
118
141
 
119
142
  const [mdReady, setMdReady] = useState(false);
120
143
  useEffect(() => {
@@ -128,31 +151,30 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
128
151
  function onMessagesClick(e: MouseEvent<HTMLDivElement>) {
129
152
  const link = (e.target as HTMLElement).closest("a");
130
153
  if (!link || !link.getAttribute("href")) return;
154
+ e.preventDefault();
131
155
  const url = new URL(link.href, window.location.href);
132
- // Native navigation is only safe when there's a hash to scroll to — the
133
- // grounding emits hashless same-pathname links (e.g. "/") for content
134
- // before the first heading, and letting those navigate natively would
135
- // full-reload the page, unmounting the panel and wiping chat history.
136
- const sameOriginWithHash = variant !== "editor" && url.origin === window.location.origin && url.hash !== "";
137
- if (sameOriginWithHash) {
138
- if (url.pathname === window.location.pathname) {
139
- return; // literally same document: native hash navigation, panel stays open
140
- }
141
- if (normalizePathname(url.pathname) === normalizePathname(window.location.pathname)) {
142
- // Equivalent pathnames (trailing-slash only difference) but NOT
143
- // literally equal — per spec, same-document fragment navigation
144
- // requires URL equality excluding the fragment, so the browser would
145
- // treat a literal mismatch as a full cross-document navigation,
146
- // unmounting the panel and wiping chat history. Perform the fragment
147
- // navigation ourselves on the current (already-correct) URL instead.
148
- e.preventDefault();
149
- window.location.hash = url.hash;
150
- return;
156
+ // Trust the hash, not the path: the model reliably preserves a section's
157
+ // #anchor but improvises the path around it (a bare "#anchor", or a path
158
+ // copied from elsewhere in the prompt). If the cited anchor exists in the
159
+ // CURRENT document — viewer page or /edit editor, both render heading
160
+ // ids — scroll to it in place; nothing navigates and the panel survives.
161
+ let anchorId = "";
162
+ if (url.hash.length > 1 && url.origin === window.location.origin) {
163
+ try {
164
+ anchorId = decodeURIComponent(url.hash.slice(1));
165
+ } catch {
166
+ anchorId = url.hash.slice(1); // malformed percent-encoding in model output
151
167
  }
152
168
  }
153
- // Cross-page (or same-page but hashless, or any /edit click): new tab so
154
- // chat history / unsaved editor state survive.
155
- e.preventDefault();
169
+ const target = anchorId ? document.getElementById(anchorId) : null;
170
+ if (target) {
171
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
172
+ window.history.replaceState(null, "", url.hash);
173
+ return;
174
+ }
175
+ // Anchor not in this document (cross-page citation on a multi-page
176
+ // portal, external URL, hashless link, or hallucinated anchor): new tab
177
+ // so chat history and any unsaved editor state survive.
156
178
  window.open(url.href, "_blank", "noopener");
157
179
  }
158
180
 
@@ -166,7 +188,7 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
166
188
  setMessages([...history, { role: "assistant", content: "" }]);
167
189
  streamingRef.current = "";
168
190
  const result = await streamChat(
169
- { messages: history, sessionId: sessionId(), ...(taskId ? { taskId } : {}), ...(draftChatbot !== undefined ? { draftChatbot } : {}) },
191
+ { messages: history, sessionId: sessionId(), ...(taskId ? { taskId } : {}), ...(draftChatbot !== undefined ? { draftChatbot } : {}), ...(simulateAudience ? { simulateAudience } : {}) },
170
192
  (delta) => {
171
193
  streamingRef.current += delta;
172
194
  const streamed = streamingRef.current;
@@ -188,20 +210,24 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
188
210
  <div role="dialog" aria-label={`Chat with ${siteName}`}
189
211
  className={cn(
190
212
  "fixed z-40 flex h-[28rem] w-80 flex-col rounded-lg border border-base-200 bg-base shadow-xl",
191
- variant === "editor" ? "bottom-40 right-4 lg:right-auto fab-container-right" : "bottom-20 right-4",
213
+ variant === "editor" ? "bottom-4 fab-panel-left max-w-[calc(100vw-5.25rem)]" : "bottom-20 right-4",
192
214
  )}>
193
215
 
194
216
  <div className="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2">
195
- <span className="text-sm font-medium text-base-contrast">{siteName} assistant</span>
196
- {tasks.length > 0 && (
197
- <select aria-label="Task" value={taskId} onChange={(e) => setTaskId(e.target.value)}
198
- className="max-w-36 rounded border border-base-200 bg-base-accent px-1 py-0.5 text-xs">
199
- <option value="">General</option>
200
- {tasks.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
201
- </select>
202
- )}
203
- <button type="button" aria-label="Close chat" onClick={onClose} className="text-base-contrast-light hover:text-base-contrast">×</button>
217
+ <span className="text-sm font-medium text-base-contrast">Portal Assistant</span>
218
+ <IconButton icon={<RemoveIcon size={18} />} label="Close chat" onClick={onClose} className="rounded-md hover:bg-base-accent" />
204
219
  </div>
220
+ {tasks.length > 0 && (
221
+ <div className="border-b border-base-200 px-3 py-2">
222
+ <Select
223
+ label="Task"
224
+ value={taskId}
225
+ onChange={setTaskId}
226
+ options={[{ value: "", label: "General" }, ...tasks.map((t) => ({ value: t.id, label: t.label }))]}
227
+ selectClassName="py-1"
228
+ />
229
+ </div>
230
+ )}
205
231
  <div className="flex-1 space-y-2 overflow-y-auto p-3" onClick={onMessagesClick}>
206
232
  {messages.map((m, i) =>
207
233
  m.role === "assistant" && mdReady && m.content ? (
@@ -219,13 +245,16 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
219
245
  )}
220
246
  {notice && <p className="text-xs text-base-contrast-light">{notice}</p>}
221
247
  </div>
222
- <form className="flex gap-2 border-t border-base-200 p-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
223
- <input
248
+ <form className="flex items-end gap-2 border-t border-base-200 p-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
249
+ <textarea
250
+ ref={composerRef}
251
+ rows={1}
224
252
  value={input}
225
253
  onChange={(e) => setInput(e.target.value)}
254
+ onKeyDown={onComposerKeyDown}
226
255
  placeholder={`Ask about ${siteName}…`}
227
256
  maxLength={4000}
228
- className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast"
257
+ className="max-h-[5.625rem] min-w-0 flex-1 resize-none overflow-y-auto rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast focus:outline-none focus:bg-[color-mix(in_srgb,var(--color-base),var(--color-base-contrast)_5%)]"
229
258
  />
230
259
  <button type="submit" aria-label="Send" disabled={busy}
231
260
  className="rounded bg-primary px-3 py-1 text-sm text-primary-contrast disabled:opacity-50">→</button>
@@ -1,10 +1,11 @@
1
- import { useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import { EditorModal } from "./EditorModal";
3
3
  import { Button } from "../shared/Button";
4
4
  import { cn } from "../../lib/cn";
5
5
  import ChatPanel from "../chat/ChatPanel";
6
6
  import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
7
7
  import type { Audience } from "../../auth/types";
8
+ import { audiencePasses } from "../../lib/pages";
8
9
 
9
10
  interface Props {
10
11
  isOpen: boolean;
@@ -39,6 +40,12 @@ export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigCh
39
40
  const chatbot = siteConfig.chatbot;
40
41
  const [newTaskLabel, setNewTaskLabel] = useState("");
41
42
  const [showTest, setShowTest] = useState(false);
43
+ const [previewAudience, setPreviewAudience] = useState("");
44
+
45
+ // A deselected audience can no longer chat — never keep simulating it.
46
+ useEffect(() => {
47
+ if (previewAudience && !chatbot.audiences.includes(previewAudience)) setPreviewAudience("");
48
+ }, [previewAudience, chatbot.audiences]);
42
49
 
43
50
  const patch = (p: Partial<ChatbotConfig>) =>
44
51
  onSiteConfigChange({ ...siteConfig, chatbot: { ...chatbot, ...p } });
@@ -67,6 +74,13 @@ export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigCh
67
74
  const toggleName = (list: string[], name: string) =>
68
75
  list.includes(name) ? list.filter((n) => n !== name) : [...list, name];
69
76
 
77
+ // Record lookups can hit inherited Object.prototype members ("constructor"
78
+ // is a legal audience name) — treat anything non-string as absent.
79
+ const overlayValue = (name: string) => {
80
+ const v = chatbot.audiencePrompts[name];
81
+ return typeof v === "string" ? v : "";
82
+ };
83
+
70
84
  return (
71
85
  <EditorModal isOpen={isOpen} onClose={onClose} title="Chatbot" size="settings" noPadding>
72
86
  <div className="flex flex-1 flex-col gap-4 overflow-y-auto px-6 py-4">
@@ -79,14 +93,27 @@ export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigCh
79
93
 
80
94
  <label className="flex items-center gap-2 text-sm text-base-contrast">
81
95
  <input type="checkbox" aria-label="Enable chatbot" checked={chatbot.enabled}
96
+ className="h-5 w-5 cursor-pointer accent-primary"
82
97
  onChange={(e) => patch({ enabled: e.target.checked })} />
83
98
  Show the chat widget to viewers
84
99
  </label>
85
100
 
86
101
  <div>
87
102
  <p className="mb-1 text-sm font-medium text-base-contrast">Audiences</p>
88
- <p className="mb-2 text-xs text-base-contrast-light">Who sees the widget. None selected = all audiences.</p>
103
+ <p className="mb-2 text-xs text-base-contrast-light">Who sees the widget — only the selected audiences; none selected means no one.</p>
89
104
  <AudienceChips audiences={audiences} selected={chatbot.audiences} onToggle={(n) => patch({ audiences: toggleName(chatbot.audiences, n) })} />
105
+ {chatbot.audiences.map((name) => (
106
+ <div key={name} className="mt-2">
107
+ <label htmlFor={`audience-prompt-${name}`} className="mb-1 block text-xs font-medium text-base-contrast">
108
+ Additional guidance for {name}
109
+ </label>
110
+ <textarea id={`audience-prompt-${name}`} aria-label={`Guidance for ${name}`} rows={2} maxLength={8000}
111
+ value={overlayValue(name)}
112
+ onChange={(e) => patch({ audiencePrompts: { ...chatbot.audiencePrompts, [name]: e.target.value } })}
113
+ placeholder={`Only ${name} sees answers shaped by this — safe for ${name}-level info.`}
114
+ className="w-full rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast" />
115
+ </div>
116
+ ))}
90
117
  </div>
91
118
 
92
119
  <div>
@@ -138,11 +165,25 @@ export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigCh
138
165
  <p className="mt-1 text-xs text-base-contrast-light">
139
166
  Tests your unsaved prompt and tasks. Answers ground on published content.
140
167
  </p>
168
+ {showTest && platformEnabled && (
169
+ <label className="mt-2 flex items-center gap-2 text-xs text-base-contrast">
170
+ Preview as
171
+ <select aria-label="Preview as" value={previewAudience} onChange={(e) => setPreviewAudience(e.target.value)}
172
+ className="rounded border border-base-200 bg-base-accent px-1 py-0.5 text-xs">
173
+ <option value="">Editor (base prompt only)</option>
174
+ {chatbot.audiences.map((n) => <option key={n} value={n}>{n}</option>)}
175
+ </select>
176
+ <span className="text-base-contrast-light">Prompt and task preview only — grounding stays editor-wide.</span>
177
+ </label>
178
+ )}
141
179
  {showTest && platformEnabled && (
142
180
  <ChatPanel
143
181
  open onClose={() => setShowTest(false)} siteName={siteConfig.siteName}
144
- tasks={chatbot.tasks.map((t) => ({ id: t.id, label: t.label }))}
182
+ tasks={chatbot.tasks
183
+ .filter((t) => previewAudience === "" || audiencePasses(t.access, previewAudience))
184
+ .map((t) => ({ id: t.id, label: t.label }))}
145
185
  isEditor draftChatbot={chatbot}
186
+ simulateAudience={previewAudience || undefined}
146
187
  />
147
188
  )}
148
189
  </div>
@@ -149,8 +149,16 @@ export const ChatbotConfigSchema = z
149
149
  .object({
150
150
  enabled: z.boolean().default(false),
151
151
  systemPrompt: z.string().max(8_000).default(""),
152
- // Audience names the widget is shown to; [] = all audiences.
152
+ // Audience names the widget is shown to — strict opt-in: [] = no viewer
153
+ // sees it (editors always can via /edit). Gates: [...slug].astro + /api/chat.
153
154
  audiences: z.array(z.string()).default([]),
155
+ // Per-audience prompt overlays, keyed by audience name. Appended AFTER the
156
+ // base systemPrompt, and ONLY for requests whose session audience matches
157
+ // (or an editor's explicit test-panel simulation) — enforced in
158
+ // assembleBrandChat. Keys are not validated against the audience list:
159
+ // entries for deselected/renamed audiences are inert (strict opt-in means
160
+ // those audiences cannot chat at all).
161
+ audiencePrompts: z.record(z.string(), z.string().max(8_000)).default({}),
154
162
  tasks: z.array(ChatbotTaskSchema).max(20).default([]),
155
163
  })
156
164
  .superRefine((cfg, ctx) => {
@@ -188,7 +196,7 @@ export const SiteConfigSchema = z.object({
188
196
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
189
197
  // sites table, not here). Every field defaults so pre-existing
190
198
  // site-config.json files keep parsing.
191
- chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [] }),
199
+ chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [], audiencePrompts: {} }),
192
200
  });
193
201
 
194
202
  export type SiteConfig = z.infer<typeof SiteConfigSchema>;