@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.
- package/dist/{chunk-WNPMBFD6.js → chunk-GKV77AA6.js} +1 -1
- package/dist/{chunk-TYYCKVAW.js → chunk-MU3CDERQ.js} +2 -2
- package/dist/{chunk-EWKCEPMW.js → chunk-NR7UB6DZ.js} +1 -1
- package/dist/{chunk-VVK6ZRCN.js → chunk-RTYJZAKR.js} +1 -1
- package/dist/{chunk-QKOIEWZ4.js → chunk-YMVYTYGQ.js} +10 -2
- package/dist/components/chat/ChatPanel.d.ts +4 -1
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/primitives/index.js +1 -1
- package/dist/components/sections/brand-guide.js +2 -2
- package/dist/components/sections/register-schemas.js +3 -3
- package/dist/components/sections/register.js +3 -3
- package/dist/components/shell/ChatbotConfigModal.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/index.js +2 -2
- package/dist/media/index.js +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +2 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatPanel.tsx +78 -49
- package/src/components/shell/ChatbotConfigModal.tsx +44 -3
- package/src/schemas/site-config.ts +10 -2
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
pluralize,
|
|
4
4
|
stripHtmlToPlainText,
|
|
5
5
|
truncate
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
46
|
+
} from "./chunk-YMVYTYGQ.js";
|
|
47
47
|
import {
|
|
48
48
|
defineSection,
|
|
49
49
|
getSection
|
|
@@ -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
|
|
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":"
|
|
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"}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ColorItemSchema,
|
|
3
3
|
ColorSpaceSchema,
|
|
4
4
|
brandGuideSectionDefs
|
|
5
|
-
} from "../../chunk-
|
|
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-
|
|
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-
|
|
4
|
-
import "../../chunk-
|
|
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-
|
|
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-
|
|
4
|
-
import "../../chunk-
|
|
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-
|
|
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;
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
36
|
+
import "../chunk-YMVYTYGQ.js";
|
|
37
37
|
import {
|
|
38
38
|
clearRegistry,
|
|
39
39
|
createRegistry,
|
package/dist/media/index.js
CHANGED
package/dist/schemas/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
AudienceNameSchema,
|
|
4
4
|
MediaGridOptionsSchema,
|
|
5
5
|
slugifyAudienceName
|
|
6
|
-
} from "../chunk-
|
|
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-
|
|
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
|
|
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,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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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-
|
|
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">
|
|
196
|
-
{
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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>;
|