@drawnagency/primitives 0.1.84 → 0.1.86
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-56J4QMF4.js → chunk-2CVP2AGK.js} +2 -2
- package/dist/{chunk-Q6LZOH5M.js → chunk-YAPG44NS.js} +1 -1
- package/dist/collab/rebase.d.ts +23 -0
- package/dist/collab/rebase.d.ts.map +1 -1
- package/dist/collab/rebase.js +12 -1
- package/dist/components/editor/index.js +4 -4
- package/dist/components/sections/register-schemas.js +2 -2
- package/dist/components/sections/register.js +2 -2
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/{ChatbotConfigModal.d.ts → SiteSettingsChatbot.d.ts} +2 -4
- package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -0
- package/dist/components/shell/SiteSettingsModal.d.ts +3 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/collab/rebase.ts +33 -0
- package/src/components/editor/ChildBlockWrapper.tsx +1 -1
- package/src/components/editor/PagesModal.tsx +39 -31
- package/src/components/editor/SectionWrapper.tsx +3 -3
- package/src/components/shared/Tabs.tsx +1 -1
- package/src/components/shell/EditorShell.tsx +25 -76
- package/src/components/shell/SiteSettingsChatbot.tsx +188 -0
- package/src/components/shell/SiteSettingsModal.tsx +29 -11
- package/dist/components/shell/ChatbotConfigModal.d.ts.map +0 -1
- package/src/components/shell/ChatbotConfigModal.tsx +0 -193
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
buildBlockDropData,
|
|
16
16
|
canDropBlock,
|
|
17
17
|
isBlockDragData
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-YAPG44NS.js";
|
|
19
19
|
import {
|
|
20
20
|
EditableRichText,
|
|
21
21
|
defineHeadingSection
|
|
@@ -544,7 +544,7 @@ var ChildBlockWrapper = forwardRef(
|
|
|
544
544
|
{
|
|
545
545
|
ref,
|
|
546
546
|
className: cn(
|
|
547
|
-
"group/childblock relative h-full rounded-
|
|
547
|
+
"group/childblock relative h-full rounded-inner",
|
|
548
548
|
isDragging && "opacity-50",
|
|
549
549
|
"hover:outline hover:outline-2 hover:outline-primary/30"
|
|
550
550
|
),
|
|
@@ -376,7 +376,7 @@ function Tabs({ tabs, defaultTabId, activeTabId, onTabChange, fullBleedTabBar })
|
|
|
376
376
|
},
|
|
377
377
|
tab.id
|
|
378
378
|
)) }) }),
|
|
379
|
-
/* @__PURE__ */ jsx8("div", { role: "tabpanel", className: cn("px-px", fullBleedTabBar && "pt-4"), children: activeTab?.content })
|
|
379
|
+
/* @__PURE__ */ jsx8("div", { role: "tabpanel", className: cn("flex min-h-0 flex-1 flex-col px-px", fullBleedTabBar && "pt-4"), children: activeTab?.content })
|
|
380
380
|
] });
|
|
381
381
|
}
|
|
382
382
|
|
package/dist/collab/rebase.d.ts
CHANGED
|
@@ -33,5 +33,28 @@ export interface RebaseResult {
|
|
|
33
33
|
* structure in both branches), and the result partitions: every section in
|
|
34
34
|
* `sections` is reachable from `index`; a dirty section deleted remotely is
|
|
35
35
|
* kept, re-linked into the index, and reported in `conflictIds`. */
|
|
36
|
+
/** Fold a peer's live (pre-save) index broadcast under our current index — the
|
|
37
|
+
* live-collab `onRemoteIndexUpdate` path, applied ONLY when we hold no unsaved
|
|
38
|
+
* structural work (so our index equals the last-saved branch: every id in it is
|
|
39
|
+
* a real, branch-backed section).
|
|
40
|
+
*
|
|
41
|
+
* Keep every peer entry whose id our current index already knows; withhold the
|
|
42
|
+
* rest, which is one of:
|
|
43
|
+
* (a) the peer's unsaved ADD — its section file is not on the branch yet, so
|
|
44
|
+
* letting it into our index would poison our next save with a dangling
|
|
45
|
+
* ref the server rejects (its live appearance is deferred to the peer's
|
|
46
|
+
* save event, which arrives via the saved-event sync); or
|
|
47
|
+
* (b) a section WE deleted unsaved — absent from our index; our deletion
|
|
48
|
+
* stands until save-time arbitration.
|
|
49
|
+
*
|
|
50
|
+
* The keep-set is our INDEX's section ids, NOT the section bodies we managed to
|
|
51
|
+
* load. A live, branch-backed section whose body was skipped by the client
|
|
52
|
+
* loader (schema/registry miss, missing file, depth trip) is still in our index
|
|
53
|
+
* and MUST survive — otherwise our next save posts an index missing it and the
|
|
54
|
+
* server rejects with "section … not found in site index" (or the write drops
|
|
55
|
+
* the live section). Taking the whole `localIndex` here, rather than a
|
|
56
|
+
* caller-computed id set, is deliberate: it makes passing the loaded-bodies set
|
|
57
|
+
* (the original bug) unrepresentable. Neither input is mutated. */
|
|
58
|
+
export declare function reconcileRemoteIndex(remoteIndex: SiteIndex, localIndex: SiteIndex): SiteIndex;
|
|
36
59
|
export declare function rebaseContent(args: RebaseArgs): RebaseResult;
|
|
37
60
|
//# sourceMappingURL=rebase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebase.d.ts","sourceRoot":"","sources":["../../src/collab/rebase.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,UAAU,EAAE,SAAS,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;;;+EAK2E;IAC3E,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,WAAW,EAAE,SAAS,CAAC;IAKvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;IAIjB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;qEAUqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CA2F5D"}
|
|
1
|
+
{"version":3,"file":"rebase.d.ts","sourceRoot":"","sources":["../../src/collab/rebase.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,UAAU,EAAE,SAAS,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;;;+EAK2E;IAC3E,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,WAAW,EAAE,SAAS,CAAC;IAKvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;IAIjB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;qEAUqE;AACrE;;;;;;;;;;;;;;;;;;;;;oEAqBoE;AACpE,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,GAAG,SAAS,CAS7F;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CA2F5D"}
|
package/dist/collab/rebase.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import "../chunk-JSBRDJBE.js";
|
|
2
2
|
|
|
3
3
|
// src/collab/rebase.ts
|
|
4
|
+
function reconcileRemoteIndex(remoteIndex, localIndex) {
|
|
5
|
+
const knownIds = new Set(Object.keys(localIndex.sections));
|
|
6
|
+
return {
|
|
7
|
+
...remoteIndex,
|
|
8
|
+
pages: remoteIndex.pages.map((p) => ({ ...p, order: p.order.filter((id) => knownIds.has(id)) })),
|
|
9
|
+
sections: Object.fromEntries(
|
|
10
|
+
Object.entries(remoteIndex.sections).filter(([id]) => knownIds.has(id))
|
|
11
|
+
)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
function rebaseContent(args) {
|
|
5
15
|
const localById = new Map(args.localSections.map((s) => [s.section.id, s]));
|
|
6
16
|
const remoteIds = new Set(args.remoteSections.map((s) => s.section.id));
|
|
@@ -60,5 +70,6 @@ function rebaseContent(args) {
|
|
|
60
70
|
return { sections: merged, index, conflictIds };
|
|
61
71
|
}
|
|
62
72
|
export {
|
|
63
|
-
rebaseContent
|
|
73
|
+
rebaseContent,
|
|
74
|
+
reconcileRemoteIndex
|
|
64
75
|
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
buildBlockDropData,
|
|
12
12
|
canDropBlock,
|
|
13
13
|
isBlockDragData
|
|
14
|
-
} from "../../chunk-
|
|
14
|
+
} from "../../chunk-YAPG44NS.js";
|
|
15
15
|
import "../../chunk-5THS4K5D.js";
|
|
16
16
|
import "../../chunk-ZSPCE6MZ.js";
|
|
17
17
|
import {
|
|
@@ -569,9 +569,9 @@ function SectionWrapper({
|
|
|
569
569
|
"relative",
|
|
570
570
|
isDragging && "opacity-50",
|
|
571
571
|
isLocked && "opacity-60",
|
|
572
|
-
isActive && "outline outline-2 outline-primary/50 rounded-
|
|
573
|
-
!isActive && showAllChrome && "outline outline-2 outline-primary/20 rounded-
|
|
574
|
-
!alwaysShow && "group-hover:outline group-hover:outline-2 group-hover:outline-primary/20 group-hover:rounded-
|
|
572
|
+
isActive && "outline outline-2 outline-primary/50 rounded-outer",
|
|
573
|
+
!isActive && showAllChrome && "outline outline-2 outline-primary/20 rounded-outer",
|
|
574
|
+
!alwaysShow && "group-hover:outline group-hover:outline-2 group-hover:outline-primary/20 group-hover:rounded-outer"
|
|
575
575
|
),
|
|
576
576
|
"data-section-id": sectionId,
|
|
577
577
|
"data-section-type": sectionType,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-2CVP2AGK.js";
|
|
4
4
|
import "../../chunk-665DW3HR.js";
|
|
5
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-YAPG44NS.js";
|
|
6
6
|
import "../../chunk-5THS4K5D.js";
|
|
7
7
|
import "../../chunk-FDBZZ4AT.js";
|
|
8
8
|
import "../../chunk-DKXISNJ2.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-2CVP2AGK.js";
|
|
4
4
|
import "../../chunk-665DW3HR.js";
|
|
5
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-YAPG44NS.js";
|
|
6
6
|
import "../../chunk-5THS4K5D.js";
|
|
7
7
|
import "../../chunk-FDBZZ4AT.js";
|
|
8
8
|
import "../../chunk-DKXISNJ2.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AASjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AASjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuDtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AAmCnH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAoBxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,EAC9B,eAAe,GAChB,EAAE,KAAK,2CAshDP;AAuED;6EAC6E;AAC7E,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,eAAe,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK/B"}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { SiteConfig } from "../../schemas/site-config";
|
|
2
2
|
import type { Audience } from "../../auth/types";
|
|
3
3
|
interface Props {
|
|
4
|
-
isOpen: boolean;
|
|
5
|
-
onClose: () => void;
|
|
6
4
|
siteConfig: SiteConfig;
|
|
7
5
|
onSiteConfigChange: (config: SiteConfig) => void;
|
|
8
6
|
audiences: Audience[];
|
|
9
7
|
platformEnabled: boolean;
|
|
10
8
|
}
|
|
11
|
-
export declare function
|
|
9
|
+
export declare function SiteSettingsChatbot({ siteConfig, onSiteConfigChange, audiences, platformEnabled }: Props): import("react/jsx-runtime").JSX.Element;
|
|
12
10
|
export {};
|
|
13
|
-
//# sourceMappingURL=
|
|
11
|
+
//# sourceMappingURL=SiteSettingsChatbot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SiteSettingsChatbot.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsChatbot.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAA8B,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGjD,UAAU,KAAK;IACb,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,mBAAmB,CAAC,EAAE,UAAU,EAAE,kBAAkB,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,KAAK,2CAwJxG"}
|
|
@@ -6,6 +6,8 @@ interface Props {
|
|
|
6
6
|
siteConfig: SiteConfig;
|
|
7
7
|
onSiteConfigChange: (config: SiteConfig) => void;
|
|
8
8
|
onAudiencesChange: (audiences: Audience[]) => void;
|
|
9
|
+
audiences: Audience[];
|
|
10
|
+
chatbotPlatformEnabled: boolean;
|
|
9
11
|
capabilities: {
|
|
10
12
|
oauth: boolean;
|
|
11
13
|
emailPassword: boolean;
|
|
@@ -19,6 +21,6 @@ interface Props {
|
|
|
19
21
|
role: "owner" | "editor";
|
|
20
22
|
} | null;
|
|
21
23
|
}
|
|
22
|
-
export declare function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, capabilities, currentUser }: Props): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, audiences, chatbotPlatformEnabled, capabilities, currentUser }: Props): import("react/jsx-runtime").JSX.Element;
|
|
23
25
|
export {};
|
|
24
26
|
//# sourceMappingURL=SiteSettingsModal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SiteSettingsModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsModal.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SiteSettingsModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsModal.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,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,iBAAiB,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACnD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,sBAAsB,EAAE,OAAO,CAAC;IAChC,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,sBAAsB,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,KAAK,2CA0G5K"}
|
package/package.json
CHANGED
package/src/collab/rebase.ts
CHANGED
|
@@ -46,6 +46,39 @@ export interface RebaseResult {
|
|
|
46
46
|
* structure in both branches), and the result partitions: every section in
|
|
47
47
|
* `sections` is reachable from `index`; a dirty section deleted remotely is
|
|
48
48
|
* kept, re-linked into the index, and reported in `conflictIds`. */
|
|
49
|
+
/** Fold a peer's live (pre-save) index broadcast under our current index — the
|
|
50
|
+
* live-collab `onRemoteIndexUpdate` path, applied ONLY when we hold no unsaved
|
|
51
|
+
* structural work (so our index equals the last-saved branch: every id in it is
|
|
52
|
+
* a real, branch-backed section).
|
|
53
|
+
*
|
|
54
|
+
* Keep every peer entry whose id our current index already knows; withhold the
|
|
55
|
+
* rest, which is one of:
|
|
56
|
+
* (a) the peer's unsaved ADD — its section file is not on the branch yet, so
|
|
57
|
+
* letting it into our index would poison our next save with a dangling
|
|
58
|
+
* ref the server rejects (its live appearance is deferred to the peer's
|
|
59
|
+
* save event, which arrives via the saved-event sync); or
|
|
60
|
+
* (b) a section WE deleted unsaved — absent from our index; our deletion
|
|
61
|
+
* stands until save-time arbitration.
|
|
62
|
+
*
|
|
63
|
+
* The keep-set is our INDEX's section ids, NOT the section bodies we managed to
|
|
64
|
+
* load. A live, branch-backed section whose body was skipped by the client
|
|
65
|
+
* loader (schema/registry miss, missing file, depth trip) is still in our index
|
|
66
|
+
* and MUST survive — otherwise our next save posts an index missing it and the
|
|
67
|
+
* server rejects with "section … not found in site index" (or the write drops
|
|
68
|
+
* the live section). Taking the whole `localIndex` here, rather than a
|
|
69
|
+
* caller-computed id set, is deliberate: it makes passing the loaded-bodies set
|
|
70
|
+
* (the original bug) unrepresentable. Neither input is mutated. */
|
|
71
|
+
export function reconcileRemoteIndex(remoteIndex: SiteIndex, localIndex: SiteIndex): SiteIndex {
|
|
72
|
+
const knownIds = new Set(Object.keys(localIndex.sections));
|
|
73
|
+
return {
|
|
74
|
+
...remoteIndex,
|
|
75
|
+
pages: remoteIndex.pages.map((p) => ({ ...p, order: p.order.filter((id) => knownIds.has(id)) })),
|
|
76
|
+
sections: Object.fromEntries(
|
|
77
|
+
Object.entries(remoteIndex.sections).filter(([id]) => knownIds.has(id)),
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
export function rebaseContent(args: RebaseArgs): RebaseResult {
|
|
50
83
|
const localById = new Map(args.localSections.map((s) => [s.section.id, s]));
|
|
51
84
|
const remoteIds = new Set(args.remoteSections.map((s) => s.section.id));
|
|
@@ -53,7 +53,7 @@ export const ChildBlockWrapper = forwardRef<HTMLDivElement, ChildBlockWrapperPro
|
|
|
53
53
|
<div
|
|
54
54
|
ref={ref}
|
|
55
55
|
className={cn(
|
|
56
|
-
"group/childblock relative h-full rounded-
|
|
56
|
+
"group/childblock relative h-full rounded-inner",
|
|
57
57
|
isDragging && "opacity-50",
|
|
58
58
|
"hover:outline hover:outline-2 hover:outline-primary/30",
|
|
59
59
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import { Check } from "lucide-react";
|
|
2
|
+
import { Check, ChevronDown, Home } from "lucide-react";
|
|
3
3
|
import type { SiteIndex, Page } from "../../schemas/site-config";
|
|
4
4
|
import type { Audience } from "../../auth/types";
|
|
5
5
|
// Sanctioned outside->inside import: document/ is a sealed subtree, but the
|
|
@@ -8,7 +8,7 @@ import type { Audience } from "../../auth/types";
|
|
|
8
8
|
import type { DocumentOptions } from "../../document/engine/settings";
|
|
9
9
|
import { Button } from "../shared/Button";
|
|
10
10
|
import { Input } from "../shared/Input";
|
|
11
|
-
import {
|
|
11
|
+
import { Toggle } from "../shared/Toggle";
|
|
12
12
|
import { AudienceIndicator } from "./AudienceIndicator";
|
|
13
13
|
import { DragHandle } from "./DragHandle";
|
|
14
14
|
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
@@ -208,30 +208,35 @@ function PageRow({
|
|
|
208
208
|
className={cn("relative rounded-md border border-base-200", isDragging && "opacity-50")}
|
|
209
209
|
>
|
|
210
210
|
<DropEdgeIndicator edge={closestEdge} />
|
|
211
|
-
<div className="flex items-
|
|
211
|
+
<div className="flex items-stretch gap-2 pl-3">
|
|
212
212
|
{draggable && (
|
|
213
|
-
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
213
|
+
<div className="flex shrink-0 items-center py-2 [&_[role=tooltip]]:hidden">
|
|
214
214
|
<DragHandle ref={handleRef} />
|
|
215
215
|
</div>
|
|
216
216
|
)}
|
|
217
|
-
<div className="min-w-0 flex-1">
|
|
218
|
-
<div className="flex items-center gap-2">
|
|
219
|
-
<span className={cn("truncate text-sm font-medium text-base-contrast", !page.title && "italic text-base-contrast/60")}>
|
|
220
|
-
{pageDisplayTitle(page.title)}
|
|
221
|
-
</span>
|
|
222
|
-
{page.isHome && <span className="rounded bg-primary px-1.5 py-0.5 text-xs font-medium text-primary-contrast">Home page</span>}
|
|
223
|
-
{!page.showInNav && <span className="rounded bg-base-100 px-1.5 py-0.5 text-xs font-medium text-base-contrast">Hidden</span>}
|
|
224
|
-
{page.status === "archived" && <span className="rounded bg-base-100 px-1.5 py-0.5 text-xs font-medium text-base-contrast">Archived</span>}
|
|
225
|
-
</div>
|
|
226
|
-
<div className="text-xs text-base-contrast/70">{page.isHome ? "/" : `/${page.slug}`}</div>
|
|
227
|
-
</div>
|
|
228
217
|
<button
|
|
229
218
|
type="button"
|
|
230
219
|
aria-label={`Edit settings for ${pageDisplayTitle(page.title)}`}
|
|
220
|
+
aria-expanded={expanded}
|
|
231
221
|
onClick={handleHeaderClick}
|
|
232
|
-
className="cursor-pointer
|
|
222
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded py-2 pr-3 pl-4 text-left hover:bg-base-accent"
|
|
233
223
|
>
|
|
234
|
-
|
|
224
|
+
<div className="min-w-0 flex-1">
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<span className={cn("truncate text-sm font-medium text-base-contrast", !page.title && "italic text-base-contrast/60")}>
|
|
227
|
+
{pageDisplayTitle(page.title)}
|
|
228
|
+
</span>
|
|
229
|
+
{page.isHome && <Home size={14} aria-label="Home page" className="shrink-0 text-base-contrast/60" />}
|
|
230
|
+
{!page.showInNav && <span className="rounded bg-base-100 px-1.5 py-0.5 text-xs font-medium text-base-contrast">Hidden</span>}
|
|
231
|
+
{page.status === "archived" && <span className="rounded bg-base-100 px-1.5 py-0.5 text-xs font-medium text-base-contrast">Archived</span>}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="text-xs text-base-contrast/70">{page.isHome ? "/" : `/${page.slug}`}</div>
|
|
234
|
+
</div>
|
|
235
|
+
<ChevronDown
|
|
236
|
+
size={16}
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
className={cn("shrink-0 text-base-contrast/50 transition-transform", expanded && "rotate-180")}
|
|
239
|
+
/>
|
|
235
240
|
</button>
|
|
236
241
|
</div>
|
|
237
242
|
|
|
@@ -254,7 +259,7 @@ function PageRow({
|
|
|
254
259
|
onChange={handleSlugChange}
|
|
255
260
|
/>
|
|
256
261
|
)}
|
|
257
|
-
<div className="flex
|
|
262
|
+
<div className="flex flex-col gap-3">
|
|
258
263
|
<div>
|
|
259
264
|
<label className="mb-1 block text-sm text-base-contrast">Audience</label>
|
|
260
265
|
<AudienceIndicator access={page.access} audiences={audiences} onChange={onSetAudience} />
|
|
@@ -264,20 +269,23 @@ function PageRow({
|
|
|
264
269
|
</p>
|
|
265
270
|
)}
|
|
266
271
|
</div>
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
<div className="flex flex-col divide-y divide-base-200 rounded-inner border border-base-200">
|
|
273
|
+
<div className="flex items-center justify-between gap-4 px-3 py-2.5">
|
|
274
|
+
<div className="min-w-0">
|
|
275
|
+
<div className="text-sm text-base-contrast">Show in menu</div>
|
|
276
|
+
<div className="text-xs text-base-contrast/70">List this page in the site navigation.</div>
|
|
277
|
+
</div>
|
|
278
|
+
<Toggle label="Show in menu" checked={page.showInNav} onChange={(v) => onSetFields({ showInNav: v })} />
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flex items-center justify-between gap-4 px-3 py-2.5">
|
|
281
|
+
<div className="min-w-0">
|
|
282
|
+
<div className="text-sm text-base-contrast">Document mode</div>
|
|
283
|
+
<div className="text-xs text-base-contrast/70">Adds a presentation view of this page.</div>
|
|
284
|
+
</div>
|
|
285
|
+
<Toggle label="Document mode" checked={page.document?.enabled ?? false} onChange={(enabled) => onSetDocument({ enabled })} />
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
274
288
|
</div>
|
|
275
|
-
<Checkbox
|
|
276
|
-
label="Document mode"
|
|
277
|
-
description="Adds a presentation view of this page."
|
|
278
|
-
checked={page.document?.enabled ?? false}
|
|
279
|
-
onChange={(enabled) => onSetDocument({ enabled })}
|
|
280
|
-
/>
|
|
281
289
|
<div className="flex items-center gap-2">
|
|
282
290
|
<Button
|
|
283
291
|
variant="secondary"
|
|
@@ -246,12 +246,12 @@ export function SectionWrapper({
|
|
|
246
246
|
"relative",
|
|
247
247
|
isDragging && "opacity-50",
|
|
248
248
|
isLocked && "opacity-60",
|
|
249
|
-
isActive && "outline outline-2 outline-primary/50 rounded-
|
|
249
|
+
isActive && "outline outline-2 outline-primary/50 rounded-outer",
|
|
250
250
|
!isActive &&
|
|
251
251
|
showAllChrome &&
|
|
252
|
-
"outline outline-2 outline-primary/20 rounded-
|
|
252
|
+
"outline outline-2 outline-primary/20 rounded-outer",
|
|
253
253
|
!alwaysShow &&
|
|
254
|
-
"group-hover:outline group-hover:outline-2 group-hover:outline-primary/20 group-hover:rounded-
|
|
254
|
+
"group-hover:outline group-hover:outline-2 group-hover:outline-primary/20 group-hover:rounded-outer",
|
|
255
255
|
)}
|
|
256
256
|
data-section-id={sectionId}
|
|
257
257
|
data-section-type={sectionType}
|
|
@@ -60,7 +60,7 @@ export function Tabs({ tabs, defaultTabId, activeTabId, onTabChange, fullBleedTa
|
|
|
60
60
|
{/* px-px: the root's overflow-hidden would clip the 1px focus ring of
|
|
61
61
|
inputs that span the full panel width (rings are box-shadows drawn
|
|
62
62
|
outside the border box) — give them 1px to paint into. */}
|
|
63
|
-
<div role="tabpanel" className={cn("px-px", fullBleedTabBar && "pt-4")}>{activeTab?.content}</div>
|
|
63
|
+
<div role="tabpanel" className={cn("flex min-h-0 flex-1 flex-col px-px", fullBleedTabBar && "pt-4")}>{activeTab?.content}</div>
|
|
64
64
|
</div>
|
|
65
65
|
);
|
|
66
66
|
}
|
|
@@ -23,7 +23,6 @@ import type { BlockMove } from "../../lib/block-move";
|
|
|
23
23
|
import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
|
|
24
24
|
import { EditorModal } from "./EditorModal";
|
|
25
25
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
26
|
-
import { ChatbotConfigModal } from "./ChatbotConfigModal";
|
|
27
26
|
import ChatWidget from "../chat/ChatWidget";
|
|
28
27
|
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
29
28
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
@@ -69,7 +68,7 @@ import {
|
|
|
69
68
|
deleteSectionRows,
|
|
70
69
|
preserveEdit,
|
|
71
70
|
} from "../../lib/dexie";
|
|
72
|
-
import { rebaseContent } from "../../collab/rebase";
|
|
71
|
+
import { rebaseContent, reconcileRemoteIndex } from "../../collab/rebase";
|
|
73
72
|
import { deriveRecoveryInputs, applyConflictChoices } from "../../collab/recovery";
|
|
74
73
|
import { getCollabProvider } from "../../collab/provider-registry";
|
|
75
74
|
import type { CollabBootstrap, CollabPeer, CollabStatus, SavedMessage, TakeoverMessage } from "../../collab/types";
|
|
@@ -100,7 +99,7 @@ import { IconButton } from "../shared/IconButton";
|
|
|
100
99
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
101
100
|
import { computeDisplaySections } from "./view-branch";
|
|
102
101
|
import { SettingsIcon } from "../shared/icons";
|
|
103
|
-
import { ImageIcon, ListOrderedIcon, FilesIcon,
|
|
102
|
+
import { ImageIcon, ListOrderedIcon, FilesIcon, JoystickIcon, X } from "lucide-react";
|
|
104
103
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
105
104
|
import { HistoryToolbar } from "./HistoryToolbar";
|
|
106
105
|
import { RestoreModal } from "./RestoreModal";
|
|
@@ -201,7 +200,6 @@ export default function EditorShell({
|
|
|
201
200
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
202
201
|
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
203
202
|
const [showPagesModal, setShowPagesModal] = useState(false);
|
|
204
|
-
const [showChatbotConfig, setShowChatbotConfig] = useState(false);
|
|
205
203
|
const [showDocumentEditor, setShowDocumentEditor] = useState(false);
|
|
206
204
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
207
205
|
const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
|
|
@@ -634,22 +632,16 @@ export default function EditorShell({
|
|
|
634
632
|
if (persistence.hasUnsavedIndex()) return;
|
|
635
633
|
const parsed = IndexSchema.safeParse(msg.index);
|
|
636
634
|
if (!parsed.success) return;
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
const pruned
|
|
647
|
-
...parsed.data,
|
|
648
|
-
pages: parsed.data.pages.map((p) => ({ ...p, order: p.order.filter((id) => localIds.has(id)) })),
|
|
649
|
-
sections: Object.fromEntries(
|
|
650
|
-
Object.entries(parsed.data.sections).filter(([id]) => localIds.has(id)),
|
|
651
|
-
),
|
|
652
|
-
};
|
|
635
|
+
// Withhold ids our current index doesn't know before applying the peer's:
|
|
636
|
+
// (a) the peer's unsaved ADD — its file isn't on the branch yet, so letting
|
|
637
|
+
// it into siteIndexRef would poison OUR next save with a dangling index ref
|
|
638
|
+
// the server rejects (its live appearance is deferred to the peer's save,
|
|
639
|
+
// via the saved-event sync); or (b) a section WE deleted unsaved — our
|
|
640
|
+
// deletion stands until save-time arbitration. Keyed on our INDEX, not the
|
|
641
|
+
// loaded section bodies: a live section whose body the loader skipped
|
|
642
|
+
// (schema/registry miss, depth trip) is still indexed and must survive —
|
|
643
|
+
// guaranteed because we only reach here with no unsaved index work.
|
|
644
|
+
const pruned = reconcileRemoteIndex(parsed.data, siteIndexRef.current);
|
|
653
645
|
setSiteIndex(pruned);
|
|
654
646
|
siteIndexRef.current = pruned;
|
|
655
647
|
},
|
|
@@ -1445,7 +1437,6 @@ export default function EditorShell({
|
|
|
1445
1437
|
onRestoreClick={() => setShowRestoreModal(true)}
|
|
1446
1438
|
onOrderingClick={() => setShowOrderingModal(true)}
|
|
1447
1439
|
onPagesClick={() => setShowPagesModal(true)}
|
|
1448
|
-
onChatbotClick={() => setShowChatbotConfig(true)}
|
|
1449
1440
|
collabPeers={collab.peers}
|
|
1450
1441
|
collabSelfId={collabBootstrap?.userId ?? null}
|
|
1451
1442
|
collabStatus={collab.status}
|
|
@@ -1577,19 +1568,11 @@ export default function EditorShell({
|
|
|
1577
1568
|
siteConfig={siteConfig!}
|
|
1578
1569
|
onSiteConfigChange={handleSiteConfigChange}
|
|
1579
1570
|
onAudiencesChange={setAudiences}
|
|
1571
|
+
audiences={audiences}
|
|
1572
|
+
chatbotPlatformEnabled={chatbotPlatformEnabled}
|
|
1580
1573
|
capabilities={capabilities}
|
|
1581
1574
|
currentUser={currentUser}
|
|
1582
1575
|
/>
|
|
1583
|
-
{siteConfig && (
|
|
1584
|
-
<ChatbotConfigModal
|
|
1585
|
-
isOpen={showChatbotConfig}
|
|
1586
|
-
onClose={() => setShowChatbotConfig(false)}
|
|
1587
|
-
siteConfig={siteConfig}
|
|
1588
|
-
onSiteConfigChange={handleSiteConfigChange}
|
|
1589
|
-
audiences={audiences}
|
|
1590
|
-
platformEnabled={chatbotPlatformEnabled}
|
|
1591
|
-
/>
|
|
1592
|
-
)}
|
|
1593
1576
|
<EditorModal
|
|
1594
1577
|
isOpen={pendingDeleteSectionId !== null}
|
|
1595
1578
|
onClose={() => setPendingDeleteSectionId(null)}
|
|
@@ -2280,7 +2263,6 @@ function EditorToolbar({
|
|
|
2280
2263
|
onRestoreClick,
|
|
2281
2264
|
onOrderingClick,
|
|
2282
2265
|
onPagesClick,
|
|
2283
|
-
onChatbotClick,
|
|
2284
2266
|
collabPeers,
|
|
2285
2267
|
collabSelfId,
|
|
2286
2268
|
collabStatus,
|
|
@@ -2303,7 +2285,6 @@ function EditorToolbar({
|
|
|
2303
2285
|
onRestoreClick: () => void;
|
|
2304
2286
|
onOrderingClick: () => void;
|
|
2305
2287
|
onPagesClick: () => void;
|
|
2306
|
-
onChatbotClick: () => void;
|
|
2307
2288
|
collabPeers: CollabPeer[];
|
|
2308
2289
|
collabSelfId: string | null;
|
|
2309
2290
|
collabStatus: CollabStatus;
|
|
@@ -2377,42 +2358,11 @@ function EditorToolbar({
|
|
|
2377
2358
|
</div>
|
|
2378
2359
|
<div className="flex items-center justify-end gap-2">
|
|
2379
2360
|
<ProcessingIndicator items={processingItems} />
|
|
2380
|
-
<IconButton
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
size="md"
|
|
2384
|
-
onClick={onOrderingClick}
|
|
2385
|
-
className="border border-base-200 bg-base-accent"
|
|
2386
|
-
/>
|
|
2387
|
-
<IconButton
|
|
2388
|
-
icon={<ImageIcon size={16} />}
|
|
2389
|
-
label="Media library"
|
|
2390
|
-
size="md"
|
|
2391
|
-
onClick={onMediaClick}
|
|
2392
|
-
className="border border-base-200 bg-base-accent"
|
|
2393
|
-
/>
|
|
2394
|
-
<IconButton
|
|
2395
|
-
icon={<FilesIcon size={16} />}
|
|
2396
|
-
label="Pages"
|
|
2397
|
-
size="md"
|
|
2398
|
-
onClick={onPagesClick}
|
|
2399
|
-
className="border border-base-200 bg-base-accent"
|
|
2400
|
-
/>
|
|
2401
|
-
<IconButton
|
|
2402
|
-
icon={<MessageCircleIcon size={16} />}
|
|
2403
|
-
label="Chatbot"
|
|
2404
|
-
size="md"
|
|
2405
|
-
onClick={onChatbotClick}
|
|
2406
|
-
className="border border-base-200 bg-base-accent"
|
|
2407
|
-
/>
|
|
2408
|
-
<IconButton
|
|
2409
|
-
icon={<SettingsIcon size={16} />}
|
|
2410
|
-
label="Site settings"
|
|
2411
|
-
size="md"
|
|
2412
|
-
onClick={onSettingsClick}
|
|
2413
|
-
className="border border-base-200 bg-base-accent"
|
|
2414
|
-
/>
|
|
2361
|
+
<IconButton icon={<ImageIcon size={16} />} label="Media library" size="md" onClick={onMediaClick} className="border border-base-200 bg-base-accent" />
|
|
2362
|
+
<IconButton icon={<ListOrderedIcon size={16} />} label="Reorder sections" size="md" onClick={onOrderingClick} className="border border-base-200 bg-base-accent" />
|
|
2363
|
+
<IconButton icon={<FilesIcon size={16} />} label="Pages" size="md" onClick={onPagesClick} className="border border-base-200 bg-base-accent" />
|
|
2415
2364
|
<ShowAllChromeToggle />
|
|
2365
|
+
<IconButton icon={<SettingsIcon size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
|
|
2416
2366
|
</div>
|
|
2417
2367
|
</div>
|
|
2418
2368
|
</div>
|
|
@@ -2457,14 +2407,13 @@ function ShowAllChromeToggle() {
|
|
|
2457
2407
|
const { showAllChrome, toggleShowAllChrome } = useEditorContext();
|
|
2458
2408
|
|
|
2459
2409
|
return (
|
|
2460
|
-
<
|
|
2461
|
-
|
|
2462
|
-
|
|
2410
|
+
<IconButton
|
|
2411
|
+
icon={<JoystickIcon size={16} />}
|
|
2412
|
+
label="Toggle editor chrome visibility"
|
|
2413
|
+
size="md"
|
|
2463
2414
|
onClick={toggleShowAllChrome}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
{showAllChrome ? "Hide Controls" : "Show Controls"}
|
|
2468
|
-
</Button>
|
|
2415
|
+
aria-pressed={showAllChrome}
|
|
2416
|
+
className="border border-base-200 bg-base-accent"
|
|
2417
|
+
/>
|
|
2469
2418
|
);
|
|
2470
2419
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Button } from "../shared/Button";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import ChatPanel from "../chat/ChatPanel";
|
|
5
|
+
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
6
|
+
import type { Audience } from "../../auth/types";
|
|
7
|
+
import { audiencePasses } from "../../lib/pages";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
siteConfig: SiteConfig;
|
|
11
|
+
onSiteConfigChange: (config: SiteConfig) => void;
|
|
12
|
+
audiences: Audience[];
|
|
13
|
+
platformEnabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function slugify(label: string): string {
|
|
17
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[]; selected: string[]; onToggle: (name: string) => void }) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-wrap gap-1">
|
|
23
|
+
{audiences.map((a) => (
|
|
24
|
+
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
25
|
+
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
26
|
+
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
27
|
+
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color } : undefined}>
|
|
28
|
+
{a.displayName}
|
|
29
|
+
</button>
|
|
30
|
+
))}
|
|
31
|
+
{audiences.length === 0 && <span className="text-xs text-base-contrast-light">No audiences configured</span>}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SiteSettingsChatbot({ siteConfig, onSiteConfigChange, audiences, platformEnabled }: Props) {
|
|
37
|
+
const chatbot = siteConfig.chatbot;
|
|
38
|
+
const [newTaskLabel, setNewTaskLabel] = useState("");
|
|
39
|
+
const [showTest, setShowTest] = useState(false);
|
|
40
|
+
const [previewAudience, setPreviewAudience] = useState("");
|
|
41
|
+
|
|
42
|
+
// A deselected audience can no longer chat — never keep simulating it.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (previewAudience && !chatbot.audiences.includes(previewAudience)) setPreviewAudience("");
|
|
45
|
+
}, [previewAudience, chatbot.audiences]);
|
|
46
|
+
|
|
47
|
+
const patch = (p: Partial<ChatbotConfig>) =>
|
|
48
|
+
onSiteConfigChange({ ...siteConfig, chatbot: { ...chatbot, ...p } });
|
|
49
|
+
|
|
50
|
+
const patchTask = (i: number, p: Partial<ChatbotTask>) =>
|
|
51
|
+
patch({ tasks: chatbot.tasks.map((t, idx) => (idx === i ? { ...t, ...p } : t)) });
|
|
52
|
+
|
|
53
|
+
function addTask() {
|
|
54
|
+
const label = newTaskLabel.trim();
|
|
55
|
+
if (!label || chatbot.tasks.length >= 20) return;
|
|
56
|
+
let id = slugify(label);
|
|
57
|
+
if (chatbot.tasks.some((t) => t.id === id)) {
|
|
58
|
+
for (let n = 2; ; n++) {
|
|
59
|
+
const suffix = `-${n}`;
|
|
60
|
+
const candidate = `${id.slice(0, 32 - suffix.length)}${suffix}`;
|
|
61
|
+
if (!chatbot.tasks.some((t) => t.id === candidate)) {
|
|
62
|
+
id = candidate;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
patch({ tasks: [...chatbot.tasks, { id, label, prompt: "", access: [] }] });
|
|
68
|
+
setNewTaskLabel("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toggleName = (list: string[], name: string) =>
|
|
72
|
+
list.includes(name) ? list.filter((n) => n !== name) : [...list, name];
|
|
73
|
+
|
|
74
|
+
// Record lookups can hit inherited Object.prototype members ("constructor"
|
|
75
|
+
// is a legal audience name) — treat anything non-string as absent.
|
|
76
|
+
const overlayValue = (name: string) => {
|
|
77
|
+
const v = chatbot.audiencePrompts[name];
|
|
78
|
+
return typeof v === "string" ? v : "";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col gap-4">
|
|
83
|
+
{!platformEnabled && (
|
|
84
|
+
<div className="rounded border border-base-200 bg-base-accent px-3 py-2 text-sm text-base-contrast">
|
|
85
|
+
Chat is not enabled for this site — contact Drawn Agency. You can still edit the
|
|
86
|
+
configuration below; it activates when chat is switched on.
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<label className="flex items-center gap-2 text-sm text-base-contrast">
|
|
91
|
+
<input type="checkbox" aria-label="Enable chatbot" checked={chatbot.enabled}
|
|
92
|
+
className="h-5 w-5 cursor-pointer accent-primary"
|
|
93
|
+
onChange={(e) => patch({ enabled: e.target.checked })} />
|
|
94
|
+
Show the chat widget to viewers
|
|
95
|
+
</label>
|
|
96
|
+
|
|
97
|
+
<div>
|
|
98
|
+
<p className="mb-1 text-sm font-medium text-base-contrast">Audiences</p>
|
|
99
|
+
<p className="mb-2 text-xs text-base-contrast-light">Who sees the widget — only the selected audiences; none selected means no one.</p>
|
|
100
|
+
<AudienceChips audiences={audiences} selected={chatbot.audiences} onToggle={(n) => patch({ audiences: toggleName(chatbot.audiences, n) })} />
|
|
101
|
+
{chatbot.audiences.map((name) => (
|
|
102
|
+
<div key={name} className="mt-2">
|
|
103
|
+
<label htmlFor={`audience-prompt-${name}`} className="mb-1 block text-xs font-medium text-base-contrast">
|
|
104
|
+
Additional guidance for {name}
|
|
105
|
+
</label>
|
|
106
|
+
<textarea id={`audience-prompt-${name}`} aria-label={`Guidance for ${name}`} rows={2} maxLength={8000}
|
|
107
|
+
value={overlayValue(name)}
|
|
108
|
+
onChange={(e) => patch({ audiencePrompts: { ...chatbot.audiencePrompts, [name]: e.target.value } })}
|
|
109
|
+
placeholder={`Only ${name} sees answers shaped by this — safe for ${name}-level info.`}
|
|
110
|
+
className="w-full rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast" />
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<label htmlFor="chatbot-system-prompt" className="mb-1 block text-sm font-medium text-base-contrast">System prompt</label>
|
|
117
|
+
<textarea id="chatbot-system-prompt" aria-label="System prompt" rows={5} maxLength={8000}
|
|
118
|
+
value={chatbot.systemPrompt}
|
|
119
|
+
onChange={(e) => patch({ systemPrompt: e.target.value })}
|
|
120
|
+
placeholder="Describe the brand's voice and how the assistant should behave — e.g. “Speak warmly and concisely; always reference our color names, never hex codes.”"
|
|
121
|
+
className="w-full rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast" />
|
|
122
|
+
<p className="text-right text-xs text-base-contrast-light">{chatbot.systemPrompt.length}/8000</p>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div>
|
|
126
|
+
<p className="mb-1 text-sm font-medium text-base-contrast">Tasks</p>
|
|
127
|
+
<p className="mb-2 text-xs text-base-contrast-light">Goal presets shown as a dropdown in the widget. Each task layers its own instructions onto the system prompt.</p>
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
{chatbot.tasks.map((t, i) => (
|
|
130
|
+
<div key={t.id} className="rounded border border-base-200 p-2">
|
|
131
|
+
<div className="mb-1 flex items-center justify-between gap-2">
|
|
132
|
+
<input aria-label={`Task ${t.id} label`} value={t.label} maxLength={60}
|
|
133
|
+
onChange={(e) => patchTask(i, { label: e.target.value })}
|
|
134
|
+
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
135
|
+
<Button type="button" variant="destructive"
|
|
136
|
+
onClick={() => patch({ tasks: chatbot.tasks.filter((_, idx) => idx !== i) })}>
|
|
137
|
+
Remove
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
<textarea aria-label={`Task ${t.id} prompt`} rows={2} maxLength={4000} value={t.prompt}
|
|
141
|
+
onChange={(e) => patchTask(i, { prompt: e.target.value })}
|
|
142
|
+
placeholder="Instructions for this task…"
|
|
143
|
+
className="mb-1 w-full rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
144
|
+
<AudienceChips audiences={audiences} selected={t.access} onToggle={(n) => patchTask(i, { access: toggleName(t.access, n) })} />
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
<div className="mt-2 flex gap-2">
|
|
149
|
+
<input aria-label="New task label" value={newTaskLabel} maxLength={60}
|
|
150
|
+
onChange={(e) => setNewTaskLabel(e.target.value)}
|
|
151
|
+
placeholder="e.g. Draft a social post"
|
|
152
|
+
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
153
|
+
<Button type="button" onClick={addTask}>Add task</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div>
|
|
158
|
+
<Button type="button" onClick={() => setShowTest((s) => !s)} disabled={!platformEnabled}>
|
|
159
|
+
{showTest ? "Hide test chat" : "Test with current draft"}
|
|
160
|
+
</Button>
|
|
161
|
+
<p className="mt-1 text-xs text-base-contrast-light">
|
|
162
|
+
Tests your unsaved prompt and tasks. Answers ground on published content.
|
|
163
|
+
</p>
|
|
164
|
+
{showTest && platformEnabled && (
|
|
165
|
+
<label className="mt-2 flex items-center gap-2 text-xs text-base-contrast">
|
|
166
|
+
Preview as
|
|
167
|
+
<select aria-label="Preview as" value={previewAudience} onChange={(e) => setPreviewAudience(e.target.value)}
|
|
168
|
+
className="rounded border border-base-200 bg-base-accent px-1 py-0.5 text-xs">
|
|
169
|
+
<option value="">Editor (base prompt only)</option>
|
|
170
|
+
{chatbot.audiences.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
171
|
+
</select>
|
|
172
|
+
<span className="text-base-contrast-light">Prompt and task preview only — grounding stays editor-wide.</span>
|
|
173
|
+
</label>
|
|
174
|
+
)}
|
|
175
|
+
{showTest && platformEnabled && (
|
|
176
|
+
<ChatPanel
|
|
177
|
+
open onClose={() => setShowTest(false)} siteName={siteConfig.siteName}
|
|
178
|
+
tasks={chatbot.tasks
|
|
179
|
+
.filter((t) => previewAudience === "" || audiencePasses(t.access, previewAudience))
|
|
180
|
+
.map((t) => ({ id: t.id, label: t.label }))}
|
|
181
|
+
isEditor draftChatbot={chatbot}
|
|
182
|
+
simulateAudience={previewAudience || undefined}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Tabs } from "../shared/Tabs";
|
|
|
5
5
|
import { SiteSettingsViewerAccess } from "./SiteSettingsViewerAccess";
|
|
6
6
|
import { SiteSettingsDisplay } from "./SiteSettingsDisplay";
|
|
7
7
|
import { SiteSettingsUsers } from "./SiteSettingsUsers";
|
|
8
|
+
import { SiteSettingsChatbot } from "./SiteSettingsChatbot";
|
|
8
9
|
import type { SiteConfig } from "../../schemas/site-config";
|
|
9
10
|
import type { Audience } from "../../auth/types";
|
|
10
11
|
|
|
@@ -14,6 +15,8 @@ interface Props {
|
|
|
14
15
|
siteConfig: SiteConfig;
|
|
15
16
|
onSiteConfigChange: (config: SiteConfig) => void;
|
|
16
17
|
onAudiencesChange: (audiences: Audience[]) => void;
|
|
18
|
+
audiences: Audience[];
|
|
19
|
+
chatbotPlatformEnabled: boolean;
|
|
17
20
|
capabilities: {
|
|
18
21
|
oauth: boolean;
|
|
19
22
|
emailPassword: boolean;
|
|
@@ -25,7 +28,7 @@ interface Props {
|
|
|
25
28
|
currentUser: { email: string; role: "owner" | "editor" } | null;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, capabilities, currentUser }: Props) {
|
|
31
|
+
export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, audiences, chatbotPlatformEnabled, capabilities, currentUser }: Props) {
|
|
29
32
|
const [signOutError, setSignOutError] = useState<string | null>(null);
|
|
30
33
|
|
|
31
34
|
async function handleSignOut() {
|
|
@@ -50,25 +53,30 @@ export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigCha
|
|
|
50
53
|
|
|
51
54
|
const allTabs = [
|
|
52
55
|
{
|
|
53
|
-
id: "
|
|
54
|
-
label: "
|
|
55
|
-
show:
|
|
56
|
+
id: "viewer-access",
|
|
57
|
+
label: "Audiences",
|
|
58
|
+
show: true,
|
|
56
59
|
content: (
|
|
57
60
|
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
58
|
-
<
|
|
61
|
+
<SiteSettingsViewerAccess
|
|
62
|
+
audienceManagement={capabilities.audienceManagement}
|
|
63
|
+
passwordToggle={capabilities.passwordToggle}
|
|
64
|
+
onAudiencesChange={onAudiencesChange}
|
|
65
|
+
/>
|
|
59
66
|
</div>
|
|
60
67
|
),
|
|
61
68
|
},
|
|
62
69
|
{
|
|
63
|
-
id: "
|
|
64
|
-
label: "
|
|
70
|
+
id: "chatbot",
|
|
71
|
+
label: "Chatbot",
|
|
65
72
|
show: true,
|
|
66
73
|
content: (
|
|
67
74
|
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
<SiteSettingsChatbot
|
|
76
|
+
siteConfig={siteConfig}
|
|
77
|
+
onSiteConfigChange={onSiteConfigChange}
|
|
78
|
+
audiences={audiences}
|
|
79
|
+
platformEnabled={chatbotPlatformEnabled}
|
|
72
80
|
/>
|
|
73
81
|
</div>
|
|
74
82
|
),
|
|
@@ -83,6 +91,16 @@ export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigCha
|
|
|
83
91
|
</div>
|
|
84
92
|
),
|
|
85
93
|
},
|
|
94
|
+
{
|
|
95
|
+
id: "users",
|
|
96
|
+
label: "Users",
|
|
97
|
+
show: capabilities.userManagement && currentUser?.role === "owner",
|
|
98
|
+
content: (
|
|
99
|
+
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
100
|
+
<SiteSettingsUsers currentUser={currentUser} />
|
|
101
|
+
</div>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
86
104
|
];
|
|
87
105
|
|
|
88
106
|
const tabItems = allTabs.filter((t) => t.show).map(({ id, label, content }) => ({ id, label, content }));
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { EditorModal } from "./EditorModal";
|
|
3
|
-
import { Button } from "../shared/Button";
|
|
4
|
-
import { cn } from "../../lib/cn";
|
|
5
|
-
import ChatPanel from "../chat/ChatPanel";
|
|
6
|
-
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
7
|
-
import type { Audience } from "../../auth/types";
|
|
8
|
-
import { audiencePasses } from "../../lib/pages";
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
isOpen: boolean;
|
|
12
|
-
onClose: () => void;
|
|
13
|
-
siteConfig: SiteConfig;
|
|
14
|
-
onSiteConfigChange: (config: SiteConfig) => void;
|
|
15
|
-
audiences: Audience[];
|
|
16
|
-
platformEnabled: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function slugify(label: string): string {
|
|
20
|
-
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[]; selected: string[]; onToggle: (name: string) => void }) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="flex flex-wrap gap-1">
|
|
26
|
-
{audiences.map((a) => (
|
|
27
|
-
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
28
|
-
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
29
|
-
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
30
|
-
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color } : undefined}>
|
|
31
|
-
{a.displayName}
|
|
32
|
-
</button>
|
|
33
|
-
))}
|
|
34
|
-
{audiences.length === 0 && <span className="text-xs text-base-contrast-light">No audiences configured</span>}
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigChange, audiences, platformEnabled }: Props) {
|
|
40
|
-
const chatbot = siteConfig.chatbot;
|
|
41
|
-
const [newTaskLabel, setNewTaskLabel] = useState("");
|
|
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]);
|
|
49
|
-
|
|
50
|
-
const patch = (p: Partial<ChatbotConfig>) =>
|
|
51
|
-
onSiteConfigChange({ ...siteConfig, chatbot: { ...chatbot, ...p } });
|
|
52
|
-
|
|
53
|
-
const patchTask = (i: number, p: Partial<ChatbotTask>) =>
|
|
54
|
-
patch({ tasks: chatbot.tasks.map((t, idx) => (idx === i ? { ...t, ...p } : t)) });
|
|
55
|
-
|
|
56
|
-
function addTask() {
|
|
57
|
-
const label = newTaskLabel.trim();
|
|
58
|
-
if (!label || chatbot.tasks.length >= 20) return;
|
|
59
|
-
let id = slugify(label);
|
|
60
|
-
if (chatbot.tasks.some((t) => t.id === id)) {
|
|
61
|
-
for (let n = 2; ; n++) {
|
|
62
|
-
const suffix = `-${n}`;
|
|
63
|
-
const candidate = `${id.slice(0, 32 - suffix.length)}${suffix}`;
|
|
64
|
-
if (!chatbot.tasks.some((t) => t.id === candidate)) {
|
|
65
|
-
id = candidate;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
patch({ tasks: [...chatbot.tasks, { id, label, prompt: "", access: [] }] });
|
|
71
|
-
setNewTaskLabel("");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const toggleName = (list: string[], name: string) =>
|
|
75
|
-
list.includes(name) ? list.filter((n) => n !== name) : [...list, name];
|
|
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
|
-
|
|
84
|
-
return (
|
|
85
|
-
<EditorModal isOpen={isOpen} onClose={onClose} title="Chatbot" size="settings" noPadding>
|
|
86
|
-
<div className="flex flex-1 flex-col gap-4 overflow-y-auto px-6 py-4">
|
|
87
|
-
{!platformEnabled && (
|
|
88
|
-
<div className="rounded border border-base-200 bg-base-accent px-3 py-2 text-sm text-base-contrast">
|
|
89
|
-
Chat is not enabled for this site — contact Drawn Agency. You can still edit the
|
|
90
|
-
configuration below; it activates when chat is switched on.
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
|
|
94
|
-
<label className="flex items-center gap-2 text-sm text-base-contrast">
|
|
95
|
-
<input type="checkbox" aria-label="Enable chatbot" checked={chatbot.enabled}
|
|
96
|
-
className="h-5 w-5 cursor-pointer accent-primary"
|
|
97
|
-
onChange={(e) => patch({ enabled: e.target.checked })} />
|
|
98
|
-
Show the chat widget to viewers
|
|
99
|
-
</label>
|
|
100
|
-
|
|
101
|
-
<div>
|
|
102
|
-
<p className="mb-1 text-sm font-medium text-base-contrast">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>
|
|
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
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div>
|
|
120
|
-
<label htmlFor="chatbot-system-prompt" className="mb-1 block text-sm font-medium text-base-contrast">System prompt</label>
|
|
121
|
-
<textarea id="chatbot-system-prompt" aria-label="System prompt" rows={5} maxLength={8000}
|
|
122
|
-
value={chatbot.systemPrompt}
|
|
123
|
-
onChange={(e) => patch({ systemPrompt: e.target.value })}
|
|
124
|
-
placeholder="Describe the brand's voice and how the assistant should behave — e.g. “Speak warmly and concisely; always reference our color names, never hex codes.”"
|
|
125
|
-
className="w-full rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast" />
|
|
126
|
-
<p className="text-right text-xs text-base-contrast-light">{chatbot.systemPrompt.length}/8000</p>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div>
|
|
130
|
-
<p className="mb-1 text-sm font-medium text-base-contrast">Tasks</p>
|
|
131
|
-
<p className="mb-2 text-xs text-base-contrast-light">Goal presets shown as a dropdown in the widget. Each task layers its own instructions onto the system prompt.</p>
|
|
132
|
-
<div className="space-y-3">
|
|
133
|
-
{chatbot.tasks.map((t, i) => (
|
|
134
|
-
<div key={t.id} className="rounded border border-base-200 p-2">
|
|
135
|
-
<div className="mb-1 flex items-center justify-between gap-2">
|
|
136
|
-
<input aria-label={`Task ${t.id} label`} value={t.label} maxLength={60}
|
|
137
|
-
onChange={(e) => patchTask(i, { label: e.target.value })}
|
|
138
|
-
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
139
|
-
<Button type="button" variant="destructive"
|
|
140
|
-
onClick={() => patch({ tasks: chatbot.tasks.filter((_, idx) => idx !== i) })}>
|
|
141
|
-
Remove
|
|
142
|
-
</Button>
|
|
143
|
-
</div>
|
|
144
|
-
<textarea aria-label={`Task ${t.id} prompt`} rows={2} maxLength={4000} value={t.prompt}
|
|
145
|
-
onChange={(e) => patchTask(i, { prompt: e.target.value })}
|
|
146
|
-
placeholder="Instructions for this task…"
|
|
147
|
-
className="mb-1 w-full rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
148
|
-
<AudienceChips audiences={audiences} selected={t.access} onToggle={(n) => patchTask(i, { access: toggleName(t.access, n) })} />
|
|
149
|
-
</div>
|
|
150
|
-
))}
|
|
151
|
-
</div>
|
|
152
|
-
<div className="mt-2 flex gap-2">
|
|
153
|
-
<input aria-label="New task label" value={newTaskLabel} maxLength={60}
|
|
154
|
-
onChange={(e) => setNewTaskLabel(e.target.value)}
|
|
155
|
-
placeholder="e.g. Draft a social post"
|
|
156
|
-
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
157
|
-
<Button type="button" onClick={addTask}>Add task</Button>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
<div>
|
|
162
|
-
<Button type="button" onClick={() => setShowTest((s) => !s)} disabled={!platformEnabled}>
|
|
163
|
-
{showTest ? "Hide test chat" : "Test with current draft"}
|
|
164
|
-
</Button>
|
|
165
|
-
<p className="mt-1 text-xs text-base-contrast-light">
|
|
166
|
-
Tests your unsaved prompt and tasks. Answers ground on published content.
|
|
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
|
-
)}
|
|
179
|
-
{showTest && platformEnabled && (
|
|
180
|
-
<ChatPanel
|
|
181
|
-
open onClose={() => setShowTest(false)} siteName={siteConfig.siteName}
|
|
182
|
-
tasks={chatbot.tasks
|
|
183
|
-
.filter((t) => previewAudience === "" || audiencePasses(t.access, previewAudience))
|
|
184
|
-
.map((t) => ({ id: t.id, label: t.label }))}
|
|
185
|
-
isEditor draftChatbot={chatbot}
|
|
186
|
-
simulateAudience={previewAudience || undefined}
|
|
187
|
-
/>
|
|
188
|
-
)}
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</EditorModal>
|
|
192
|
-
);
|
|
193
|
-
}
|