@drawnagency/primitives 0.1.68 → 0.1.70
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/components/chat/ChatPanel.d.ts +8 -2
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.d.ts +4 -1
- package/dist/components/chat/ChatWidget.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/components/chat/ChatPanel.tsx +95 -11
- package/src/components/chat/ChatWidget.tsx +40 -10
- package/src/components/shell/BugReportFAB.tsx +1 -1
- package/src/components/shell/EditorShell.tsx +16 -1
|
@@ -2,6 +2,10 @@ 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[];
|
|
@@ -16,7 +20,7 @@ export declare function streamChat(body: {
|
|
|
16
20
|
error: string;
|
|
17
21
|
retryAfterMs?: number;
|
|
18
22
|
}>;
|
|
19
|
-
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot }: {
|
|
23
|
+
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, variant }: {
|
|
20
24
|
open: boolean;
|
|
21
25
|
onClose: () => void;
|
|
22
26
|
siteName: string;
|
|
@@ -25,7 +29,9 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
25
29
|
label: string;
|
|
26
30
|
}[];
|
|
27
31
|
isEditor?: boolean;
|
|
28
|
-
/** Set
|
|
32
|
+
/** Set by the configurator's test panel and the /edit chat FAB. */
|
|
29
33
|
draftChatbot?: unknown;
|
|
34
|
+
/** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
|
|
35
|
+
variant?: "viewer" | "editor";
|
|
30
36
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
31
37
|
//# sourceMappingURL=ChatPanel.d.ts.map
|
|
@@ -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":"AAKA,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AA6B5E;;kEAEkE;AAClE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhD;AAcD,8EAA8E;AAC9E,wBAAsB,UAAU,CAC9B,IAAI,EAAE;IAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EAC7F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCjG;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAkB,EAAE,EAAE;IAChH,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mFAAmF;IACnF,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAC/B,kDA6HA"}
|
|
@@ -6,6 +6,9 @@ export interface ChatWidgetProps {
|
|
|
6
6
|
label: string;
|
|
7
7
|
}[];
|
|
8
8
|
isEditor?: boolean;
|
|
9
|
+
/** Editor-only: chat against the current (unsaved) chatbot config. */
|
|
10
|
+
draftChatbot?: unknown;
|
|
11
|
+
variant?: "viewer" | "editor";
|
|
9
12
|
}
|
|
10
|
-
export default function ChatWidget({ siteName, tasks, isEditor }: ChatWidgetProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export default function ChatWidget({ siteName, tasks, isEditor, draftChatbot, variant }: ChatWidgetProps): import("react/jsx-runtime").JSX.Element;
|
|
11
14
|
//# sourceMappingURL=ChatWidget.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatWidget.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ChatWidget.tsx"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -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;
|
|
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;AA0EjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,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;CAClC;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,GAC/B,EAAE,KAAK,2CAi8BP"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
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,10 +1,21 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
|
2
|
+
import { marked } from "marked";
|
|
2
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { ensureSanitizer, sanitizeHtml } from "../../lib/sanitize";
|
|
3
5
|
|
|
4
6
|
export interface ChatMessage { role: "user" | "assistant"; content: string }
|
|
5
7
|
|
|
6
8
|
const SESSION_KEY = "bp-chat-session";
|
|
7
9
|
|
|
10
|
+
// Astro's trailingSlash: "ignore" means the viewer can be on "/brand/" while
|
|
11
|
+
// grounding links say "/brand#voice-tone" — the pathnames are equivalent
|
|
12
|
+
// but not literally equal. Per the HTML spec, same-document fragment
|
|
13
|
+
// navigation requires URL equality *excluding* the fragment, so a literal
|
|
14
|
+
// mismatch (e.g. "/brand/" vs "/brand") can't rely on native navigation —
|
|
15
|
+
// this helper only detects that "equivalent but not equal" case so we can
|
|
16
|
+
// handle it ourselves. "/" stays "/" rather than collapsing to "".
|
|
17
|
+
const normalizePathname = (p: string): string => p.replace(/\/+$/, "") || "/";
|
|
18
|
+
|
|
8
19
|
function sessionId(): string {
|
|
9
20
|
let id = window.localStorage.getItem(SESSION_KEY);
|
|
10
21
|
if (!id) {
|
|
@@ -21,6 +32,25 @@ const ERROR_COPY: Record<string, string> = {
|
|
|
21
32
|
upstream_error: "Something went wrong — please try again.",
|
|
22
33
|
};
|
|
23
34
|
|
|
35
|
+
/** Assistant markdown → sanitized HTML. Callers MUST gate on the sanitizer
|
|
36
|
+
* having loaded (`mdReady`) — before then sanitizeHtml passes raw HTML
|
|
37
|
+
* through, and model output is untrusted. Exported for tests. */
|
|
38
|
+
export function assistantHtml(md: string): string {
|
|
39
|
+
return sanitizeHtml(marked.parse(md, { async: false, gfm: true, breaks: true }) as string);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tailwind preflight makes h1–h6 inherit size/weight, so headings only need
|
|
43
|
+
// weight + spacing to clamp to the bubble's text scale.
|
|
44
|
+
const MD_STYLES =
|
|
45
|
+
"[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 " +
|
|
46
|
+
"[&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 " +
|
|
47
|
+
"[&_a]:underline [&_a]:underline-offset-2 " +
|
|
48
|
+
"[&_code]:rounded [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] " +
|
|
49
|
+
"[&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-base-200 [&_pre]:p-2 " +
|
|
50
|
+
"[&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold " +
|
|
51
|
+
"[&_h3]:mt-2 [&_h3]:font-semibold [&_h4]:mt-2 [&_h4]:font-semibold " +
|
|
52
|
+
"[&_blockquote]:mt-2 [&_blockquote]:border-l-2 [&_blockquote]:border-base-200 [&_blockquote]:pl-2";
|
|
53
|
+
|
|
24
54
|
/** Parse our /api/chat SSE frames out of a fetch body. Exported for tests. */
|
|
25
55
|
export async function streamChat(
|
|
26
56
|
body: { messages: ChatMessage[]; sessionId: string; taskId?: string; draftChatbot?: unknown },
|
|
@@ -68,14 +98,16 @@ export async function streamChat(
|
|
|
68
98
|
}
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot }: {
|
|
101
|
+
export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, draftChatbot, variant = "viewer" }: {
|
|
72
102
|
open: boolean;
|
|
73
103
|
onClose: () => void;
|
|
74
104
|
siteName: string;
|
|
75
105
|
tasks: { id: string; label: string }[];
|
|
76
106
|
isEditor?: boolean;
|
|
77
|
-
/** Set
|
|
107
|
+
/** Set by the configurator's test panel and the /edit chat FAB. */
|
|
78
108
|
draftChatbot?: unknown;
|
|
109
|
+
/** Anchors the panel above its launcher: viewer corner FAB vs /edit FAB column. */
|
|
110
|
+
variant?: "viewer" | "editor";
|
|
79
111
|
}) {
|
|
80
112
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
81
113
|
const [taskId, setTaskId] = useState<string>("");
|
|
@@ -84,6 +116,46 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
84
116
|
const [notice, setNotice] = useState<string | null>(null);
|
|
85
117
|
const streamingRef = useRef("");
|
|
86
118
|
|
|
119
|
+
const [mdReady, setMdReady] = useState(false);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let alive = true;
|
|
122
|
+
// Failure just leaves mdReady false forever (plain-text messages) — never
|
|
123
|
+
// let it surface as an unhandled rejection.
|
|
124
|
+
void ensureSanitizer().then(() => { if (alive) setMdReady(true); }).catch(() => {});
|
|
125
|
+
return () => { alive = false; };
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
function onMessagesClick(e: MouseEvent<HTMLDivElement>) {
|
|
129
|
+
const link = (e.target as HTMLElement).closest("a");
|
|
130
|
+
if (!link || !link.getAttribute("href")) return;
|
|
131
|
+
const url = new URL(link.href, window.location.href);
|
|
132
|
+
// Native navigation is only safe when there's a hash to scroll to — the
|
|
133
|
+
// grounding emits hashless same-pathname links (e.g. "/") for content
|
|
134
|
+
// before the first heading, and letting those navigate natively would
|
|
135
|
+
// full-reload the page, unmounting the panel and wiping chat history.
|
|
136
|
+
const sameOriginWithHash = variant !== "editor" && url.origin === window.location.origin && url.hash !== "";
|
|
137
|
+
if (sameOriginWithHash) {
|
|
138
|
+
if (url.pathname === window.location.pathname) {
|
|
139
|
+
return; // literally same document: native hash navigation, panel stays open
|
|
140
|
+
}
|
|
141
|
+
if (normalizePathname(url.pathname) === normalizePathname(window.location.pathname)) {
|
|
142
|
+
// Equivalent pathnames (trailing-slash only difference) but NOT
|
|
143
|
+
// literally equal — per spec, same-document fragment navigation
|
|
144
|
+
// requires URL equality excluding the fragment, so the browser would
|
|
145
|
+
// treat a literal mismatch as a full cross-document navigation,
|
|
146
|
+
// unmounting the panel and wiping chat history. Perform the fragment
|
|
147
|
+
// navigation ourselves on the current (already-correct) URL instead.
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
window.location.hash = url.hash;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Cross-page (or same-page but hashless, or any /edit click): new tab so
|
|
154
|
+
// chat history / unsaved editor state survive.
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
window.open(url.href, "_blank", "noopener");
|
|
157
|
+
}
|
|
158
|
+
|
|
87
159
|
async function send() {
|
|
88
160
|
const text = input.trim();
|
|
89
161
|
if (!text || busy) return;
|
|
@@ -114,7 +186,11 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
114
186
|
|
|
115
187
|
return (
|
|
116
188
|
<div role="dialog" aria-label={`Chat with ${siteName}`}
|
|
117
|
-
className=
|
|
189
|
+
className={cn(
|
|
190
|
+
"fixed z-40 flex h-[28rem] w-80 flex-col rounded-lg border border-base-200 bg-base shadow-xl",
|
|
191
|
+
variant === "editor" ? "bottom-40 right-4 lg:right-auto fab-container-right" : "bottom-20 right-4",
|
|
192
|
+
)}>
|
|
193
|
+
|
|
118
194
|
<div className="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2">
|
|
119
195
|
<span className="text-sm font-medium text-base-contrast">{siteName} assistant</span>
|
|
120
196
|
{tasks.length > 0 && (
|
|
@@ -126,13 +202,21 @@ export default function ChatPanel({ open, onClose, siteName, tasks, isEditor, dr
|
|
|
126
202
|
)}
|
|
127
203
|
<button type="button" aria-label="Close chat" onClick={onClose} className="text-base-contrast-light hover:text-base-contrast">×</button>
|
|
128
204
|
</div>
|
|
129
|
-
<div className="flex-1 space-y-2 overflow-y-auto p-3">
|
|
130
|
-
{messages.map((m, i) =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
205
|
+
<div className="flex-1 space-y-2 overflow-y-auto p-3" onClick={onMessagesClick}>
|
|
206
|
+
{messages.map((m, i) =>
|
|
207
|
+
m.role === "assistant" && mdReady && m.content ? (
|
|
208
|
+
<div
|
|
209
|
+
key={i}
|
|
210
|
+
className={cn("max-w-[85%] rounded-lg bg-base-accent px-3 py-2 text-sm text-base-contrast", MD_STYLES)}
|
|
211
|
+
dangerouslySetInnerHTML={{ __html: assistantHtml(m.content) }}
|
|
212
|
+
/>
|
|
213
|
+
) : (
|
|
214
|
+
<div key={i} className={cn("max-w-[85%] whitespace-pre-wrap rounded-lg px-3 py-2 text-sm",
|
|
215
|
+
m.role === "user" ? "ml-auto bg-primary text-primary-contrast" : "bg-base-accent text-base-contrast")}>
|
|
216
|
+
{m.content || (busy && i === messages.length - 1 ? "…" : m.content)}
|
|
217
|
+
</div>
|
|
218
|
+
),
|
|
219
|
+
)}
|
|
136
220
|
{notice && <p className="text-xs text-base-contrast-light">{notice}</p>}
|
|
137
221
|
</div>
|
|
138
222
|
<form className="flex gap-2 border-t border-base-200 p-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Chat launcher FAB. Renders ONLY the floating button until first open; the
|
|
2
|
+
// panel (and its code) loads lazily so viewers who never click pay almost
|
|
3
|
+
// nothing. Browser-safe: no node imports, no process.env.
|
|
4
|
+
//
|
|
5
|
+
// Two placements:
|
|
6
|
+
// - "viewer" (default): bottom-right corner of viewer pages.
|
|
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
|
+
// via fab-container-right like its neighbors.
|
|
4
10
|
import { lazy, Suspense, useState } from "react";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
5
12
|
|
|
6
13
|
const ChatPanel = lazy(() => import("./ChatPanel"));
|
|
7
14
|
|
|
@@ -10,9 +17,12 @@ export interface ChatWidgetProps {
|
|
|
10
17
|
/** Already audience-filtered server-side — never pass unfiltered config tasks. */
|
|
11
18
|
tasks: { id: string; label: string }[];
|
|
12
19
|
isEditor?: boolean;
|
|
20
|
+
/** Editor-only: chat against the current (unsaved) chatbot config. */
|
|
21
|
+
draftChatbot?: unknown;
|
|
22
|
+
variant?: "viewer" | "editor";
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
export default function ChatWidget({ siteName, tasks, isEditor }: ChatWidgetProps) {
|
|
25
|
+
export default function ChatWidget({ siteName, tasks, isEditor, draftChatbot, variant = "viewer" }: ChatWidgetProps) {
|
|
16
26
|
const [open, setOpen] = useState(false);
|
|
17
27
|
const [everOpened, setEverOpened] = useState(false);
|
|
18
28
|
|
|
@@ -20,17 +30,37 @@ export default function ChatWidget({ siteName, tasks, isEditor }: ChatWidgetProp
|
|
|
20
30
|
<>
|
|
21
31
|
<button
|
|
22
32
|
type="button"
|
|
23
|
-
aria-label={`Chat with ${siteName}`}
|
|
33
|
+
aria-label={open ? "Collapse chat" : `Chat with ${siteName}`}
|
|
34
|
+
aria-expanded={open}
|
|
24
35
|
onClick={() => { setOpen((o) => !o); setEverOpened(true); }}
|
|
25
|
-
className=
|
|
36
|
+
className={cn(
|
|
37
|
+
"fixed z-40 flex items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity",
|
|
38
|
+
variant === "editor"
|
|
39
|
+
? "bottom-4 right-4 lg:right-auto h-10 w-10 fab-container-right"
|
|
40
|
+
: "bottom-4 right-4 h-12 w-12",
|
|
41
|
+
)}
|
|
26
42
|
>
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
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
|
+
)}
|
|
30
52
|
</button>
|
|
31
53
|
{everOpened && (
|
|
32
54
|
<Suspense fallback={null}>
|
|
33
|
-
<ChatPanel
|
|
55
|
+
<ChatPanel
|
|
56
|
+
open={open}
|
|
57
|
+
onClose={() => setOpen(false)}
|
|
58
|
+
siteName={siteName}
|
|
59
|
+
tasks={tasks}
|
|
60
|
+
isEditor={isEditor}
|
|
61
|
+
draftChatbot={draftChatbot}
|
|
62
|
+
variant={variant}
|
|
63
|
+
/>
|
|
34
64
|
</Suspense>
|
|
35
65
|
)}
|
|
36
66
|
</>
|
|
@@ -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">
|
|
@@ -20,6 +20,7 @@ import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
|
|
|
20
20
|
import { EditorModal } from "./EditorModal";
|
|
21
21
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
22
22
|
import { ChatbotConfigModal } from "./ChatbotConfigModal";
|
|
23
|
+
import ChatWidget from "../chat/ChatWidget";
|
|
23
24
|
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
24
25
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
25
26
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
@@ -902,6 +903,20 @@ export default function EditorShell({
|
|
|
902
903
|
the button would always 401 — hide it there. */}
|
|
903
904
|
{!capabilities.passwordOnly && <BugReportFAB />}
|
|
904
905
|
|
|
906
|
+
{/* Editor chat FAB: chat while editing, against the DRAFT chatbot
|
|
907
|
+
config (tasks + prompt reflect unsaved changes; grounding is
|
|
908
|
+
published content). Gated only on the platform switch — editors
|
|
909
|
+
may chat before the widget is enabled for viewers. */}
|
|
910
|
+
{chatbotPlatformEnabled && siteConfig && (
|
|
911
|
+
<ChatWidget
|
|
912
|
+
siteName={siteConfig.siteName}
|
|
913
|
+
tasks={siteConfig.chatbot.tasks.map((t) => ({ id: t.id, label: t.label }))}
|
|
914
|
+
isEditor
|
|
915
|
+
draftChatbot={siteConfig.chatbot}
|
|
916
|
+
variant="editor"
|
|
917
|
+
/>
|
|
918
|
+
)}
|
|
919
|
+
|
|
905
920
|
{rejectedUploads.length > 0 && (
|
|
906
921
|
<div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
|
|
907
922
|
<UploadRejectionAlert
|
|
@@ -1681,7 +1696,7 @@ function EditorToolbar({
|
|
|
1681
1696
|
{!historyState && (
|
|
1682
1697
|
<button
|
|
1683
1698
|
onClick={toggleEditMode}
|
|
1684
|
-
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"
|
|
1685
1700
|
aria-label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
|
|
1686
1701
|
>
|
|
1687
1702
|
{isEditMode ? (
|