@drawnagency/primitives 0.1.69 → 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 +8 -1
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.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 +2 -1
- package/src/components/chat/ChatPanel.tsx +131 -24
- package/src/components/chat/ChatWidget.tsx +14 -7
- package/src/components/shell/BugReportFAB.tsx +1 -1
- package/src/components/shell/ChatbotConfigModal.tsx +44 -3
- package/src/components/shell/EditorShell.tsx +1 -1
- 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 {
|
|
@@ -2,12 +2,17 @@ export interface ChatMessage {
|
|
|
2
2
|
role: "user" | "assistant";
|
|
3
3
|
content: string;
|
|
4
4
|
}
|
|
5
|
+
/** Assistant markdown → sanitized HTML. Callers MUST gate on the sanitizer
|
|
6
|
+
* having loaded (`mdReady`) — before then sanitizeHtml passes raw HTML
|
|
7
|
+
* through, and model output is untrusted. Exported for tests. */
|
|
8
|
+
export declare function assistantHtml(md: string): string;
|
|
5
9
|
/** Parse our /api/chat SSE frames out of a fetch body. Exported for tests. */
|
|
6
10
|
export declare function streamChat(body: {
|
|
7
11
|
messages: ChatMessage[];
|
|
8
12
|
sessionId: string;
|
|
9
13
|
taskId?: string;
|
|
10
14
|
draftChatbot?: unknown;
|
|
15
|
+
simulateAudience?: string;
|
|
11
16
|
}, onText: (text: string) => void): Promise<{
|
|
12
17
|
ok: true;
|
|
13
18
|
truncated: boolean;
|
|
@@ -16,7 +21,7 @@ export declare function streamChat(body: {
|
|
|
16
21
|
error: string;
|
|
17
22
|
retryAfterMs?: number;
|
|
18
23
|
}>;
|
|
19
|
-
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, variant }: {
|
|
24
|
+
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, simulateAudience, variant }: {
|
|
20
25
|
open: boolean;
|
|
21
26
|
onClose: () => void;
|
|
22
27
|
siteName: string;
|
|
@@ -27,6 +32,8 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
27
32
|
isEditor?: boolean;
|
|
28
33
|
/** Set by the configurator's test panel and the /edit chat FAB. */
|
|
29
34
|
draftChatbot?: unknown;
|
|
35
|
+
/** Configurator test panel only: preview this audience's prompt overlay. */
|
|
36
|
+
simulateAudience?: string;
|
|
30
37
|
/** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
|
|
31
38
|
variant?: "viewer" | "editor";
|
|
32
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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatWidget.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ChatWidget.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC/B;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAkB,EAAE,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"ChatWidget.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ChatWidget.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC/B;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAkB,EAAE,EAAE,eAAe,2CA2ClH"}
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"dompurify": "^3.2.6",
|
|
107
107
|
"jose": "^6.2.2",
|
|
108
108
|
"lucide-react": "^1.7.0",
|
|
109
|
+
"marked": "^18.0.5",
|
|
109
110
|
"tailwind-merge": "^3.5.0",
|
|
110
111
|
"zod": "^4.3.6"
|
|
111
112
|
},
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState, type KeyboardEvent, type MouseEvent } from "react";
|
|
2
|
+
import { marked } from "marked";
|
|
2
3
|
import { cn } from "../../lib/cn";
|
|
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";
|
|
3
8
|
|
|
4
9
|
export interface ChatMessage { role: "user" | "assistant"; content: string }
|
|
5
10
|
|
|
@@ -21,9 +26,28 @@ const ERROR_COPY: Record<string, string> = {
|
|
|
21
26
|
upstream_error: "Something went wrong — please try again.",
|
|
22
27
|
};
|
|
23
28
|
|
|
29
|
+
/** Assistant markdown → sanitized HTML. Callers MUST gate on the sanitizer
|
|
30
|
+
* having loaded (`mdReady`) — before then sanitizeHtml passes raw HTML
|
|
31
|
+
* through, and model output is untrusted. Exported for tests. */
|
|
32
|
+
export function assistantHtml(md: string): string {
|
|
33
|
+
return sanitizeHtml(marked.parse(md, { async: false, gfm: true, breaks: true }) as string);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tailwind preflight makes h1–h6 inherit size/weight, so headings only need
|
|
37
|
+
// weight + spacing to clamp to the bubble's text scale.
|
|
38
|
+
const MD_STYLES =
|
|
39
|
+
"[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 " +
|
|
40
|
+
"[&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 " +
|
|
41
|
+
"[&_a]:underline [&_a]:underline-offset-2 [&_a]:text-primary " +
|
|
42
|
+
"[&_code]:rounded [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] " +
|
|
43
|
+
"[&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-base-200 [&_pre]:p-2 " +
|
|
44
|
+
"[&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold " +
|
|
45
|
+
"[&_h3]:mt-2 [&_h3]:font-semibold [&_h4]:mt-2 [&_h4]:font-semibold " +
|
|
46
|
+
"[&_blockquote]:mt-2 [&_blockquote]:border-l-2 [&_blockquote]:border-base-200 [&_blockquote]:pl-2";
|
|
47
|
+
|
|
24
48
|
/** Parse our /api/chat SSE frames out of a fetch body. Exported for tests. */
|
|
25
49
|
export async function streamChat(
|
|
26
|
-
body: { messages: ChatMessage[]; sessionId: string; taskId?: string; draftChatbot?: unknown },
|
|
50
|
+
body: { messages: ChatMessage[]; sessionId: string; taskId?: string; draftChatbot?: unknown; simulateAudience?: string },
|
|
27
51
|
onText: (text: string) => void,
|
|
28
52
|
): Promise<{ ok: true; truncated: boolean } | { ok: false; error: string; retryAfterMs?: number }> {
|
|
29
53
|
try {
|
|
@@ -68,7 +92,7 @@ export async function streamChat(
|
|
|
68
92
|
}
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
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" }: {
|
|
72
96
|
open: boolean;
|
|
73
97
|
onClose: () => void;
|
|
74
98
|
siteName: string;
|
|
@@ -76,6 +100,8 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
76
100
|
isEditor?: boolean;
|
|
77
101
|
/** Set by the configurator's test panel and the /edit chat FAB. */
|
|
78
102
|
draftChatbot?: unknown;
|
|
103
|
+
/** Configurator test panel only: preview this audience's prompt overlay. */
|
|
104
|
+
simulateAudience?: string;
|
|
79
105
|
/** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
|
|
80
106
|
variant?: "viewer" | "editor";
|
|
81
107
|
}) {
|
|
@@ -85,6 +111,72 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
85
111
|
const [busy, setBusy] = useState(false);
|
|
86
112
|
const [notice, setNotice] = useState<string | null>(null);
|
|
87
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]);
|
|
141
|
+
|
|
142
|
+
const [mdReady, setMdReady] = useState(false);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
let alive = true;
|
|
145
|
+
// Failure just leaves mdReady false forever (plain-text messages) — never
|
|
146
|
+
// let it surface as an unhandled rejection.
|
|
147
|
+
void ensureSanitizer().then(() => { if (alive) setMdReady(true); }).catch(() => {});
|
|
148
|
+
return () => { alive = false; };
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
function onMessagesClick(e: MouseEvent<HTMLDivElement>) {
|
|
152
|
+
const link = (e.target as HTMLElement).closest("a");
|
|
153
|
+
if (!link || !link.getAttribute("href")) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
const url = new URL(link.href, window.location.href);
|
|
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
|
|
167
|
+
}
|
|
168
|
+
}
|
|
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.
|
|
178
|
+
window.open(url.href, "_blank", "noopener");
|
|
179
|
+
}
|
|
88
180
|
|
|
89
181
|
async function send() {
|
|
90
182
|
const text = input.trim();
|
|
@@ -96,7 +188,7 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
96
188
|
setMessages([...history, { role: "assistant", content: "" }]);
|
|
97
189
|
streamingRef.current = "";
|
|
98
190
|
const result = await streamChat(
|
|
99
|
-
{ messages: history, sessionId: sessionId(), ...(taskId ? { taskId } : {}), ...(draftChatbot !== undefined ? { draftChatbot } : {}) },
|
|
191
|
+
{ messages: history, sessionId: sessionId(), ...(taskId ? { taskId } : {}), ...(draftChatbot !== undefined ? { draftChatbot } : {}), ...(simulateAudience ? { simulateAudience } : {}) },
|
|
100
192
|
(delta) => {
|
|
101
193
|
streamingRef.current += delta;
|
|
102
194
|
const streamed = streamingRef.current;
|
|
@@ -118,36 +210,51 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
118
210
|
<div role="dialog" aria-label={`Chat with ${siteName}`}
|
|
119
211
|
className={cn(
|
|
120
212
|
"fixed z-40 flex h-[28rem] w-80 flex-col rounded-lg border border-base-200 bg-base shadow-xl",
|
|
121
|
-
variant === "editor" ? "bottom-
|
|
213
|
+
variant === "editor" ? "bottom-4 fab-panel-left max-w-[calc(100vw-5.25rem)]" : "bottom-20 right-4",
|
|
122
214
|
)}>
|
|
123
215
|
|
|
124
216
|
<div className="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2">
|
|
125
|
-
<span className="text-sm font-medium text-base-contrast">
|
|
126
|
-
{
|
|
127
|
-
<select aria-label="Task" value={taskId} onChange={(e) => setTaskId(e.target.value)}
|
|
128
|
-
className="max-w-36 rounded border border-base-200 bg-base-accent px-1 py-0.5 text-xs">
|
|
129
|
-
<option value="">General</option>
|
|
130
|
-
{tasks.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
|
|
131
|
-
</select>
|
|
132
|
-
)}
|
|
133
|
-
<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" />
|
|
134
219
|
</div>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
)}
|
|
231
|
+
<div className="flex-1 space-y-2 overflow-y-auto p-3" onClick={onMessagesClick}>
|
|
232
|
+
{messages.map((m, i) =>
|
|
233
|
+
m.role === "assistant" && mdReady && m.content ? (
|
|
234
|
+
<div
|
|
235
|
+
key={i}
|
|
236
|
+
className={cn("max-w-[85%] rounded-lg bg-base-accent px-3 py-2 text-sm text-base-contrast", MD_STYLES)}
|
|
237
|
+
dangerouslySetInnerHTML={{ __html: assistantHtml(m.content) }}
|
|
238
|
+
/>
|
|
239
|
+
) : (
|
|
240
|
+
<div key={i} className={cn("max-w-[85%] whitespace-pre-wrap rounded-lg px-3 py-2 text-sm",
|
|
241
|
+
m.role === "user" ? "ml-auto bg-primary text-primary-contrast" : "bg-base-accent text-base-contrast")}>
|
|
242
|
+
{m.content || (busy && i === messages.length - 1 ? "…" : m.content)}
|
|
243
|
+
</div>
|
|
244
|
+
),
|
|
245
|
+
)}
|
|
142
246
|
{notice && <p className="text-xs text-base-contrast-light">{notice}</p>}
|
|
143
247
|
</div>
|
|
144
|
-
<form className="flex gap-2 border-t border-base-200 p-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
|
145
|
-
<
|
|
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}
|
|
146
252
|
value={input}
|
|
147
253
|
onChange={(e) => setInput(e.target.value)}
|
|
254
|
+
onKeyDown={onComposerKeyDown}
|
|
148
255
|
placeholder={`Ask about ${siteName}…`}
|
|
149
256
|
maxLength={4000}
|
|
150
|
-
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%)]"
|
|
151
258
|
/>
|
|
152
259
|
<button type="submit" aria-label="Send" disabled={busy}
|
|
153
260
|
className="rounded bg-primary px-3 py-1 text-sm text-primary-contrast disabled:opacity-50">→</button>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Two placements:
|
|
6
6
|
// - "viewer" (default): bottom-right corner of viewer pages.
|
|
7
|
-
// - "editor":
|
|
8
|
-
//
|
|
7
|
+
// - "editor": BOTTOM of the /edit route's FAB column (edit-mode toggle at
|
|
8
|
+
// bottom-16, bug report at bottom-28 above it), aligned to the content edge
|
|
9
9
|
// via fab-container-right like its neighbors.
|
|
10
10
|
import { lazy, Suspense, useState } from "react";
|
|
11
11
|
import { cn } from "../../lib/cn";
|
|
@@ -30,18 +30,25 @@ export default function ChatWidget({ siteName, tasks, isEditor, draftChatbot, va
|
|
|
30
30
|
<>
|
|
31
31
|
<button
|
|
32
32
|
type="button"
|
|
33
|
-
aria-label={`Chat with ${siteName}`}
|
|
33
|
+
aria-label={open ? "Collapse chat" : `Chat with ${siteName}`}
|
|
34
|
+
aria-expanded={open}
|
|
34
35
|
onClick={() => { setOpen((o) => !o); setEverOpened(true); }}
|
|
35
36
|
className={cn(
|
|
36
37
|
"fixed z-40 flex items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity",
|
|
37
38
|
variant === "editor"
|
|
38
|
-
? "bottom-
|
|
39
|
+
? "bottom-4 right-4 lg:right-auto h-10 w-10 fab-container-right"
|
|
39
40
|
: "bottom-4 right-4 h-12 w-12",
|
|
40
41
|
)}
|
|
41
42
|
>
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
43
|
+
{open ? (
|
|
44
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={variant === "editor" ? "h-5 w-5" : "h-6 w-6"} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
45
|
+
<path d="m6 9 6 6 6-6" />
|
|
46
|
+
</svg>
|
|
47
|
+
) : (
|
|
48
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={variant === "editor" ? "h-5 w-5" : "h-6 w-6"} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
49
|
+
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
|
|
50
|
+
</svg>
|
|
51
|
+
)}
|
|
45
52
|
</button>
|
|
46
53
|
{everOpened && (
|
|
47
54
|
<Suspense fallback={null}>
|
|
@@ -190,7 +190,7 @@ export function BugReportFAB() {
|
|
|
190
190
|
type="button"
|
|
191
191
|
onClick={handleOpen}
|
|
192
192
|
aria-label="Report a bug"
|
|
193
|
-
className="cursor-pointer fixed bottom-
|
|
193
|
+
className="cursor-pointer fixed bottom-28 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
194
194
|
>
|
|
195
195
|
{/* Bug / beetle SVG icon */}
|
|
196
196
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
@@ -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>
|
|
@@ -1696,7 +1696,7 @@ function EditorToolbar({
|
|
|
1696
1696
|
{!historyState && (
|
|
1697
1697
|
<button
|
|
1698
1698
|
onClick={toggleEditMode}
|
|
1699
|
-
className="cursor-pointer fixed bottom-
|
|
1699
|
+
className="cursor-pointer fixed bottom-16 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
1700
1700
|
aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
|
|
1701
1701
|
>
|
|
1702
1702
|
{isEditMode ? (
|
|
@@ -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>;
|