@webmaster-droid/web 0.1.0-alpha.0

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/index.js ADDED
@@ -0,0 +1,1600 @@
1
+ "use client";
2
+
3
+ // src/editables.tsx
4
+ import {
5
+ createContext,
6
+ createElement,
7
+ useContext
8
+ } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ var EDITABLE_ROOTS = ["pages.", "layout.", "seo.", "themeTokens."];
11
+ var MAX_PATH_LENGTH = 320;
12
+ var MAX_LABEL_LENGTH = 120;
13
+ var MAX_PREVIEW_LENGTH = 140;
14
+ var EditableContext = createContext(null);
15
+ function EditableProvider(props) {
16
+ return /* @__PURE__ */ jsx(
17
+ EditableContext.Provider,
18
+ {
19
+ value: {
20
+ document: props.document,
21
+ mode: props.mode ?? "live",
22
+ enabled: props.enabled ?? true
23
+ },
24
+ children: props.children
25
+ }
26
+ );
27
+ }
28
+ function useEditableDocument() {
29
+ const context = useContext(EditableContext);
30
+ if (!context) {
31
+ throw new Error("useEditableDocument must be used within <EditableProvider>");
32
+ }
33
+ return context;
34
+ }
35
+ function splitPath(path) {
36
+ return path.replace(/\[(\d+)\]/g, ".$1").split(".").map((segment) => segment.trim()).filter(Boolean);
37
+ }
38
+ function readByPath(input, path) {
39
+ const segments = splitPath(path);
40
+ let current = input;
41
+ for (const segment of segments) {
42
+ if (Array.isArray(current)) {
43
+ const index = Number(segment);
44
+ if (Number.isNaN(index)) {
45
+ return void 0;
46
+ }
47
+ current = current[index];
48
+ continue;
49
+ }
50
+ if (typeof current !== "object" || current === null) {
51
+ return void 0;
52
+ }
53
+ current = current[segment];
54
+ }
55
+ return current;
56
+ }
57
+ function normalizeEditablePath(value) {
58
+ if (typeof value !== "string") {
59
+ return null;
60
+ }
61
+ const trimmed = value.trim();
62
+ if (!trimmed || trimmed.length > MAX_PATH_LENGTH) {
63
+ return null;
64
+ }
65
+ if (!EDITABLE_ROOTS.some((prefix) => trimmed.startsWith(prefix))) {
66
+ return null;
67
+ }
68
+ return trimmed;
69
+ }
70
+ function normalizeShortText(value, maxLength) {
71
+ if (typeof value !== "string") {
72
+ return null;
73
+ }
74
+ const compact = value.replace(/\s+/g, " ").trim();
75
+ if (!compact) {
76
+ return null;
77
+ }
78
+ if (compact.length <= maxLength) {
79
+ return compact;
80
+ }
81
+ return `${compact.slice(0, maxLength - 3)}...`;
82
+ }
83
+ function normalizeKind(value) {
84
+ if (value === "text" || value === "image" || value === "link" || value === "section") {
85
+ return value;
86
+ }
87
+ return null;
88
+ }
89
+ function normalizeRelatedPaths(value) {
90
+ if (!Array.isArray(value)) {
91
+ return [];
92
+ }
93
+ const out = /* @__PURE__ */ new Set();
94
+ for (const item of value) {
95
+ const normalized = normalizeEditablePath(item);
96
+ if (normalized) {
97
+ out.add(normalized);
98
+ }
99
+ }
100
+ return Array.from(out);
101
+ }
102
+ function normalizePagePath(value) {
103
+ if (typeof value !== "string") {
104
+ return "/";
105
+ }
106
+ const [withoutQuery] = value.split(/[?#]/, 1);
107
+ const trimmed = withoutQuery?.trim() ?? "";
108
+ if (!trimmed.startsWith("/")) {
109
+ return "/";
110
+ }
111
+ return trimmed || "/";
112
+ }
113
+ function editableMeta(input) {
114
+ const path = normalizeEditablePath(input.path);
115
+ const label = normalizeShortText(input.label, MAX_LABEL_LENGTH);
116
+ const kind = normalizeKind(input.kind);
117
+ if (!path || !label || !kind) {
118
+ return {};
119
+ }
120
+ const attrs = {
121
+ "data-wmd-path": path,
122
+ "data-wmd-label": label,
123
+ "data-wmd-kind": kind
124
+ };
125
+ const relatedPaths = normalizeRelatedPaths(input.relatedPaths ?? []);
126
+ if (relatedPaths.length > 0) {
127
+ attrs["data-wmd-related-paths"] = JSON.stringify(relatedPaths);
128
+ }
129
+ const preview = normalizeShortText(input.preview, MAX_PREVIEW_LENGTH);
130
+ if (preview) {
131
+ attrs["data-wmd-preview"] = preview;
132
+ }
133
+ return attrs;
134
+ }
135
+ function parseSelectedEditableFromTarget(target, pagePath) {
136
+ const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
137
+ if (!targetElement) {
138
+ return null;
139
+ }
140
+ const element = targetElement.closest("[data-wmd-path][data-wmd-label][data-wmd-kind]");
141
+ if (!element) {
142
+ return null;
143
+ }
144
+ const path = normalizeEditablePath(element.dataset.wmdPath);
145
+ const label = normalizeShortText(element.dataset.wmdLabel, MAX_LABEL_LENGTH);
146
+ const kind = normalizeKind(element.dataset.wmdKind);
147
+ if (!path || !label || !kind) {
148
+ return null;
149
+ }
150
+ let parsedRelated = [];
151
+ const rawRelated = element.dataset.wmdRelatedPaths;
152
+ if (rawRelated) {
153
+ try {
154
+ parsedRelated = JSON.parse(rawRelated);
155
+ } catch {
156
+ parsedRelated = [];
157
+ }
158
+ }
159
+ const relatedPaths = normalizeRelatedPaths(parsedRelated);
160
+ const preview = normalizeShortText(element.dataset.wmdPreview, MAX_PREVIEW_LENGTH);
161
+ const selected = {
162
+ path,
163
+ label,
164
+ kind,
165
+ pagePath: normalizePagePath(pagePath)
166
+ };
167
+ if (relatedPaths.length > 0) {
168
+ selected.relatedPaths = relatedPaths;
169
+ }
170
+ if (preview) {
171
+ selected.preview = preview;
172
+ }
173
+ return selected;
174
+ }
175
+ function pickStringValue(document2, path, fallback) {
176
+ const value = readByPath(document2, path);
177
+ return typeof value === "string" && value.trim() ? value : fallback;
178
+ }
179
+ function EditableText({
180
+ path,
181
+ fallback,
182
+ as = "span",
183
+ label,
184
+ relatedPaths,
185
+ ...rest
186
+ }) {
187
+ const { document: document2, enabled } = useEditableDocument();
188
+ const value = pickStringValue(document2, path, fallback);
189
+ const attrs = enabled ? editableMeta({
190
+ path,
191
+ label: label ?? path,
192
+ kind: "text",
193
+ relatedPaths,
194
+ preview: value
195
+ }) : {};
196
+ return createElement(as, { ...rest, ...attrs }, value);
197
+ }
198
+ function EditableRichText({
199
+ path,
200
+ fallback,
201
+ as = "div",
202
+ label,
203
+ ...rest
204
+ }) {
205
+ const { document: document2, enabled } = useEditableDocument();
206
+ const value = pickStringValue(document2, path, fallback);
207
+ const attrs = enabled ? editableMeta({
208
+ path,
209
+ label: label ?? path,
210
+ kind: "section",
211
+ preview: value
212
+ }) : {};
213
+ return createElement(as, {
214
+ ...rest,
215
+ ...attrs,
216
+ dangerouslySetInnerHTML: { __html: value }
217
+ });
218
+ }
219
+ function EditableImage({
220
+ path,
221
+ fallbackSrc,
222
+ altPath,
223
+ fallbackAlt = "",
224
+ label,
225
+ ...rest
226
+ }) {
227
+ const { document: document2, enabled } = useEditableDocument();
228
+ const src = pickStringValue(document2, path, fallbackSrc);
229
+ const alt = altPath ? pickStringValue(document2, altPath, fallbackAlt) : fallbackAlt;
230
+ const attrs = enabled ? editableMeta({
231
+ path,
232
+ label: label ?? path,
233
+ kind: "image",
234
+ relatedPaths: altPath ? [altPath] : [],
235
+ preview: src
236
+ }) : {};
237
+ return /* @__PURE__ */ jsx("img", { ...rest, ...attrs, src, alt });
238
+ }
239
+ function EditableLink({
240
+ hrefPath,
241
+ labelPath,
242
+ fallbackHref,
243
+ fallbackLabel,
244
+ label,
245
+ ...rest
246
+ }) {
247
+ const { document: document2, enabled } = useEditableDocument();
248
+ const href = pickStringValue(document2, hrefPath, fallbackHref);
249
+ const text = pickStringValue(document2, labelPath, fallbackLabel);
250
+ const attrs = enabled ? editableMeta({
251
+ path: labelPath,
252
+ label: label ?? labelPath,
253
+ kind: "link",
254
+ relatedPaths: [hrefPath],
255
+ preview: href
256
+ }) : {};
257
+ return /* @__PURE__ */ jsx("a", { ...rest, ...attrs, href, children: text });
258
+ }
259
+
260
+ // src/config.ts
261
+ var DEFAULT_CONFIG = {
262
+ apiBaseUrl: "",
263
+ supabaseUrl: "",
264
+ supabaseAnonKey: "",
265
+ modeQueryParam: "mode",
266
+ modeQueryValue: "admin",
267
+ modeStorageKey: "webmaster_droid_admin_mode",
268
+ defaultModelId: "openai:gpt-5.2",
269
+ assistantAvatarUrl: "/assets/admin/webmaster-avatar.png",
270
+ assistantAvatarFallback: "W"
271
+ };
272
+ function normalizeOptionalString(value) {
273
+ if (typeof value !== "string") {
274
+ return "";
275
+ }
276
+ return value.trim();
277
+ }
278
+ function normalizeApiBaseUrl(value) {
279
+ if (!value) {
280
+ return "";
281
+ }
282
+ return value.replace(/\/$/, "");
283
+ }
284
+ function resolveWebmasterDroidConfig(input) {
285
+ const apiBaseUrl = normalizeApiBaseUrl(
286
+ normalizeOptionalString(input?.apiBaseUrl ?? process.env.NEXT_PUBLIC_AGENT_API_BASE_URL)
287
+ );
288
+ const supabaseUrl = normalizeOptionalString(
289
+ input?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL
290
+ );
291
+ const supabaseAnonKey = normalizeOptionalString(
292
+ input?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
293
+ );
294
+ const modeQueryParam = normalizeOptionalString(input?.modeQueryParam) || DEFAULT_CONFIG.modeQueryParam;
295
+ const modeQueryValue = normalizeOptionalString(input?.modeQueryValue) || DEFAULT_CONFIG.modeQueryValue;
296
+ const modeStorageKey = normalizeOptionalString(input?.modeStorageKey) || DEFAULT_CONFIG.modeStorageKey;
297
+ const defaultModelId = normalizeOptionalString(input?.defaultModelId) || DEFAULT_CONFIG.defaultModelId;
298
+ const assistantAvatarUrl = normalizeOptionalString(input?.assistantAvatarUrl) || DEFAULT_CONFIG.assistantAvatarUrl;
299
+ const assistantAvatarFallback = normalizeOptionalString(input?.assistantAvatarFallback) || DEFAULT_CONFIG.assistantAvatarFallback;
300
+ return {
301
+ apiBaseUrl,
302
+ supabaseUrl,
303
+ supabaseAnonKey,
304
+ modeQueryParam,
305
+ modeQueryValue,
306
+ modeStorageKey,
307
+ defaultModelId,
308
+ assistantAvatarUrl,
309
+ assistantAvatarFallback
310
+ };
311
+ }
312
+ function buildApiUrl(apiBaseUrl, path) {
313
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
314
+ return `${normalizeApiBaseUrl(apiBaseUrl)}${normalizedPath}`;
315
+ }
316
+
317
+ // src/context.tsx
318
+ import {
319
+ createContext as createContext2,
320
+ useContext as useContext2,
321
+ useEffect,
322
+ useMemo,
323
+ useState
324
+ } from "react";
325
+
326
+ // src/api.ts
327
+ function withAuthHeaders(token) {
328
+ if (!token) {
329
+ return {};
330
+ }
331
+ return {
332
+ Authorization: `Bearer ${token}`
333
+ };
334
+ }
335
+ async function fetchCmsContent(apiBaseUrl, stage, token) {
336
+ const response = await fetch(buildApiUrl(apiBaseUrl, `/api/content?stage=${stage}`), {
337
+ headers: {
338
+ ...withAuthHeaders(token)
339
+ },
340
+ cache: "no-store"
341
+ });
342
+ if (!response.ok) {
343
+ throw new Error(`Failed to fetch ${stage} content.`);
344
+ }
345
+ const payload = await response.json();
346
+ return payload.content;
347
+ }
348
+ async function fetchModels(apiBaseUrl) {
349
+ const response = await fetch(buildApiUrl(apiBaseUrl, "/api/models"), {
350
+ cache: "no-store"
351
+ });
352
+ if (!response.ok) {
353
+ throw new Error("Failed to fetch model configuration.");
354
+ }
355
+ return await response.json();
356
+ }
357
+ async function fetchHistory(apiBaseUrl, token) {
358
+ const response = await fetch(buildApiUrl(apiBaseUrl, "/api/history"), {
359
+ headers: {
360
+ ...withAuthHeaders(token)
361
+ },
362
+ cache: "no-store"
363
+ });
364
+ if (!response.ok) {
365
+ throw new Error("Failed to fetch history.");
366
+ }
367
+ return await response.json();
368
+ }
369
+ async function publishDraft(apiBaseUrl, token, body) {
370
+ const response = await fetch(buildApiUrl(apiBaseUrl, "/api/publish"), {
371
+ method: "POST",
372
+ headers: {
373
+ "content-type": "application/json",
374
+ ...withAuthHeaders(token)
375
+ },
376
+ body: JSON.stringify(body)
377
+ });
378
+ if (!response.ok) {
379
+ const detail = await response.text();
380
+ throw new Error(`Publish failed: ${detail}`);
381
+ }
382
+ return response.json();
383
+ }
384
+ async function rollbackDraft(apiBaseUrl, token, body) {
385
+ const response = await fetch(buildApiUrl(apiBaseUrl, "/api/rollback"), {
386
+ method: "POST",
387
+ headers: {
388
+ "content-type": "application/json",
389
+ ...withAuthHeaders(token)
390
+ },
391
+ body: JSON.stringify(body)
392
+ });
393
+ if (!response.ok) {
394
+ const detail = await response.text();
395
+ throw new Error(`Rollback failed: ${detail}`);
396
+ }
397
+ return response.json();
398
+ }
399
+ async function deleteCheckpoint(apiBaseUrl, token, body) {
400
+ const response = await fetch(buildApiUrl(apiBaseUrl, "/api/checkpoints/delete"), {
401
+ method: "POST",
402
+ headers: {
403
+ "content-type": "application/json",
404
+ ...withAuthHeaders(token)
405
+ },
406
+ body: JSON.stringify(body)
407
+ });
408
+ if (!response.ok) {
409
+ const detail = await response.text();
410
+ throw new Error(`Delete checkpoint failed: ${detail}`);
411
+ }
412
+ return response.json();
413
+ }
414
+ function parseEventChunk(chunk) {
415
+ const normalized = chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
416
+ const lines = normalized.split("\n");
417
+ const parsed = [];
418
+ let eventName = "message";
419
+ let dataLines = [];
420
+ const flush = () => {
421
+ if (dataLines.length === 0) {
422
+ eventName = "message";
423
+ return;
424
+ }
425
+ parsed.push({
426
+ event: eventName || "message",
427
+ data: dataLines.join("\n")
428
+ });
429
+ eventName = "message";
430
+ dataLines = [];
431
+ };
432
+ for (const line of lines) {
433
+ if (line === "") {
434
+ flush();
435
+ continue;
436
+ }
437
+ if (line.startsWith(":")) {
438
+ continue;
439
+ }
440
+ const separatorIndex = line.indexOf(":");
441
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
442
+ let value = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
443
+ if (value.startsWith(" ")) {
444
+ value = value.slice(1);
445
+ }
446
+ if (field === "event") {
447
+ eventName = value || "message";
448
+ continue;
449
+ }
450
+ if (field === "data") {
451
+ dataLines.push(value);
452
+ }
453
+ }
454
+ return parsed;
455
+ }
456
+ function yieldToUi() {
457
+ return new Promise((resolve) => {
458
+ setTimeout(resolve, 0);
459
+ });
460
+ }
461
+ async function streamChat(params) {
462
+ const response = await fetch(buildApiUrl(params.apiBaseUrl, "/api/chat/stream"), {
463
+ method: "POST",
464
+ headers: {
465
+ "content-type": "application/json",
466
+ Accept: "text/event-stream",
467
+ ...withAuthHeaders(params.token)
468
+ },
469
+ body: JSON.stringify({
470
+ message: params.message,
471
+ modelId: params.modelId,
472
+ includeThinking: params.includeThinking,
473
+ currentPath: params.currentPath,
474
+ selectedElement: params.selectedElement ?? null,
475
+ history: params.history
476
+ })
477
+ });
478
+ if (!response.ok) {
479
+ const detail = await response.text();
480
+ throw new Error(`Chat request failed: ${detail}`);
481
+ }
482
+ if (!response.body) {
483
+ throw new Error("Missing response body for SSE stream.");
484
+ }
485
+ const reader = response.body.getReader();
486
+ const decoder = new TextDecoder();
487
+ let buffer = "";
488
+ while (true) {
489
+ const { done, value } = await reader.read();
490
+ if (done) {
491
+ break;
492
+ }
493
+ buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
494
+ const chunks = buffer.split("\n\n");
495
+ buffer = chunks.pop() ?? "";
496
+ for (const chunk of chunks) {
497
+ const events = parseEventChunk(`${chunk}
498
+
499
+ `);
500
+ for (const event of events) {
501
+ let payload = event.data;
502
+ try {
503
+ payload = JSON.parse(event.data);
504
+ } catch {
505
+ payload = event.data;
506
+ }
507
+ params.onEvent({
508
+ event: event.event,
509
+ data: payload
510
+ });
511
+ await yieldToUi();
512
+ }
513
+ }
514
+ }
515
+ const remainder = buffer.trim();
516
+ if (!remainder) {
517
+ return;
518
+ }
519
+ for (const event of parseEventChunk(`${remainder}
520
+
521
+ `)) {
522
+ let payload = event.data;
523
+ try {
524
+ payload = JSON.parse(event.data);
525
+ } catch {
526
+ payload = event.data;
527
+ }
528
+ params.onEvent({
529
+ event: event.event,
530
+ data: payload
531
+ });
532
+ await yieldToUi();
533
+ }
534
+ }
535
+
536
+ // src/supabase-client.ts
537
+ import { createClient } from "@supabase/supabase-js";
538
+ var supabaseClientCache = /* @__PURE__ */ new Map();
539
+ function getSupabaseBrowserClient(config) {
540
+ if (!config.supabaseUrl || !config.supabaseAnonKey) {
541
+ return null;
542
+ }
543
+ const cacheKey = `${config.supabaseUrl}::${config.supabaseAnonKey}`;
544
+ const cached = supabaseClientCache.get(cacheKey);
545
+ if (cached) {
546
+ return cached;
547
+ }
548
+ const client = createClient(config.supabaseUrl, config.supabaseAnonKey, {
549
+ auth: {
550
+ persistSession: true,
551
+ autoRefreshToken: true,
552
+ detectSessionInUrl: true
553
+ }
554
+ });
555
+ supabaseClientCache.set(cacheKey, client);
556
+ return client;
557
+ }
558
+
559
+ // src/context.tsx
560
+ import { jsx as jsx2 } from "react/jsx-runtime";
561
+ var WebmasterDroidContext = createContext2(null);
562
+ function WebmasterDroidProvider(props) {
563
+ const resolvedConfig = useMemo(
564
+ () => resolveWebmasterDroidConfig(props.config),
565
+ [props.config]
566
+ );
567
+ const [isAdminMode, setIsAdminMode] = useState(false);
568
+ useEffect(() => {
569
+ const checkMode = () => {
570
+ const params = new URLSearchParams(window.location.search);
571
+ const modeValue = params.get(resolvedConfig.modeQueryParam);
572
+ if (modeValue === resolvedConfig.modeQueryValue) {
573
+ window.sessionStorage.setItem(resolvedConfig.modeStorageKey, "1");
574
+ }
575
+ const persisted = window.sessionStorage.getItem(resolvedConfig.modeStorageKey);
576
+ setIsAdminMode(modeValue === resolvedConfig.modeQueryValue || persisted === "1");
577
+ };
578
+ checkMode();
579
+ window.addEventListener("popstate", checkMode);
580
+ return () => {
581
+ window.removeEventListener("popstate", checkMode);
582
+ };
583
+ }, [resolvedConfig.modeQueryParam, resolvedConfig.modeQueryValue, resolvedConfig.modeStorageKey]);
584
+ const supabase = useMemo(
585
+ () => getSupabaseBrowserClient(resolvedConfig),
586
+ [resolvedConfig]
587
+ );
588
+ const authConfigured = Boolean(supabase);
589
+ const [session, setSession] = useState(null);
590
+ const [modelId, setModelId] = useState(null);
591
+ const [showModelPickerState, setShowModelPickerState] = useState(false);
592
+ const [modelOptions, setModelOptions] = useState([]);
593
+ const [includeThinking, setIncludeThinking] = useState(false);
594
+ const [refreshKey, setRefreshKey] = useState(0);
595
+ const [selectedElement, setSelectedElement] = useState(null);
596
+ useEffect(() => {
597
+ let ignore = false;
598
+ if (!isAdminMode || !supabase) {
599
+ return;
600
+ }
601
+ supabase.auth.getSession().then(({ data: data2 }) => {
602
+ if (!ignore) {
603
+ setSession(data2.session ?? null);
604
+ }
605
+ });
606
+ const { data } = supabase.auth.onAuthStateChange((_event, nextSession) => {
607
+ setSession(nextSession);
608
+ });
609
+ return () => {
610
+ ignore = true;
611
+ data.subscription.unsubscribe();
612
+ };
613
+ }, [isAdminMode, supabase]);
614
+ useEffect(() => {
615
+ let ignore = false;
616
+ if (!isAdminMode) {
617
+ return;
618
+ }
619
+ fetchModels(resolvedConfig.apiBaseUrl).then((models) => {
620
+ if (ignore) {
621
+ return;
622
+ }
623
+ const options = models.availableModels ?? [];
624
+ const preferredDefault = options.some((option) => option.id === models.defaultModelId) ? models.defaultModelId : options[0]?.id ?? models.defaultModelId;
625
+ setShowModelPickerState(models.showModelPicker);
626
+ setModelOptions(options);
627
+ setModelId((current) => {
628
+ if (current && options.some((option) => option.id === current)) {
629
+ return current;
630
+ }
631
+ return preferredDefault;
632
+ });
633
+ }).catch(() => {
634
+ if (!ignore) {
635
+ setShowModelPickerState(false);
636
+ setModelOptions([]);
637
+ setModelId((current) => current ?? resolvedConfig.defaultModelId);
638
+ }
639
+ });
640
+ return () => {
641
+ ignore = true;
642
+ };
643
+ }, [isAdminMode, resolvedConfig.apiBaseUrl, resolvedConfig.defaultModelId]);
644
+ const activeSession = isAdminMode ? session : null;
645
+ const showModelPicker = isAdminMode ? showModelPickerState : false;
646
+ const value = useMemo(
647
+ () => ({
648
+ config: resolvedConfig,
649
+ isAdminMode,
650
+ session: activeSession,
651
+ token: activeSession?.access_token ?? null,
652
+ isAuthenticated: Boolean(activeSession?.access_token),
653
+ modelId,
654
+ setModelId,
655
+ showModelPicker,
656
+ modelOptions,
657
+ includeThinking,
658
+ setIncludeThinking,
659
+ refreshKey,
660
+ requestRefresh: () => setRefreshKey((x) => x + 1),
661
+ authConfigured,
662
+ selectedElement,
663
+ setSelectedElement,
664
+ clearSelectedElement: () => setSelectedElement(null)
665
+ }),
666
+ [
667
+ resolvedConfig,
668
+ isAdminMode,
669
+ activeSession,
670
+ modelId,
671
+ showModelPicker,
672
+ modelOptions,
673
+ includeThinking,
674
+ refreshKey,
675
+ authConfigured,
676
+ selectedElement
677
+ ]
678
+ );
679
+ return /* @__PURE__ */ jsx2(WebmasterDroidContext.Provider, { value, children: props.children });
680
+ }
681
+ function useWebmasterDroid() {
682
+ const context = useContext2(WebmasterDroidContext);
683
+ if (!context) {
684
+ throw new Error("useWebmasterDroid must be used within <WebmasterDroidProvider>");
685
+ }
686
+ return context;
687
+ }
688
+
689
+ // src/overlay.tsx
690
+ import {
691
+ useCallback,
692
+ useEffect as useEffect2,
693
+ useMemo as useMemo2,
694
+ useRef,
695
+ useState as useState2
696
+ } from "react";
697
+ import ReactMarkdown from "react-markdown";
698
+ import remarkGfm from "remark-gfm";
699
+ import { REQUIRED_PUBLISH_CONFIRMATION } from "@webmaster-droid/contracts";
700
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
701
+ function createMessage(role, text, status) {
702
+ return {
703
+ id: `${role}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
704
+ role,
705
+ text,
706
+ status: role === "assistant" ? status ?? "final" : void 0
707
+ };
708
+ }
709
+ function insertBeforePendingMessage(entries, message, pendingAssistantId) {
710
+ if (!pendingAssistantId) {
711
+ return [...entries, message];
712
+ }
713
+ const pendingIndex = entries.findIndex((entry) => entry.id === pendingAssistantId);
714
+ if (pendingIndex === -1) {
715
+ return [...entries, message];
716
+ }
717
+ const next = [...entries];
718
+ next.splice(pendingIndex, 0, message);
719
+ return next;
720
+ }
721
+ function removeMessageById(entries, messageId) {
722
+ if (!messageId) {
723
+ return entries;
724
+ }
725
+ return entries.filter((entry) => entry.id !== messageId);
726
+ }
727
+ function resolvePendingAssistant(entries, pendingAssistantId, text) {
728
+ if (!pendingAssistantId) {
729
+ return { nextEntries: entries, replaced: false };
730
+ }
731
+ let replaced = false;
732
+ const nextEntries = entries.map((entry) => {
733
+ if (entry.id !== pendingAssistantId) {
734
+ return entry;
735
+ }
736
+ replaced = true;
737
+ return {
738
+ ...entry,
739
+ text,
740
+ status: "final"
741
+ };
742
+ });
743
+ return { nextEntries, replaced };
744
+ }
745
+ function formatHistoryTime(value) {
746
+ const parsed = new Date(value);
747
+ if (Number.isNaN(parsed.getTime())) {
748
+ return value;
749
+ }
750
+ return new Intl.DateTimeFormat("en-IN", {
751
+ dateStyle: "medium",
752
+ timeStyle: "short"
753
+ }).format(parsed);
754
+ }
755
+ function historyTimestamp(value) {
756
+ const parsed = Date.parse(value);
757
+ return Number.isNaN(parsed) ? null : parsed;
758
+ }
759
+ function toReadableToolLine(toolName, summary) {
760
+ const normalized = summary.trim();
761
+ if (!normalized) {
762
+ return toolName.replace(/_/g, " ");
763
+ }
764
+ const prefixedPattern = new RegExp(`^${toolName.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\s*:\\s*`, "i");
765
+ const withoutToolPrefix = normalized.replace(prefixedPattern, "");
766
+ const withoutTechnicalPrefix = withoutToolPrefix.replace(/^[a-z0-9_]+:\s*/i, "");
767
+ return withoutTechnicalPrefix || normalized;
768
+ }
769
+ function buildModelHistory(entries) {
770
+ return entries.filter(
771
+ (entry) => entry.role === "user" || entry.role === "assistant"
772
+ ).slice(-12).map((entry) => ({
773
+ role: entry.role,
774
+ text: entry.text
775
+ }));
776
+ }
777
+ function kindIcon(kind) {
778
+ if (kind === "image") {
779
+ return "IMG";
780
+ }
781
+ if (kind === "link") {
782
+ return "LNK";
783
+ }
784
+ if (kind === "section") {
785
+ return "SEC";
786
+ }
787
+ return "TXT";
788
+ }
789
+ function WebmasterDroidOverlay() {
790
+ const {
791
+ config,
792
+ isAdminMode,
793
+ isAuthenticated,
794
+ token,
795
+ modelId,
796
+ setModelId,
797
+ showModelPicker,
798
+ modelOptions,
799
+ includeThinking,
800
+ requestRefresh,
801
+ authConfigured,
802
+ selectedElement,
803
+ setSelectedElement,
804
+ clearSelectedElement
805
+ } = useWebmasterDroid();
806
+ const [isOpen, setIsOpen] = useState2(false);
807
+ const [activeTab, setActiveTab] = useState2("chat");
808
+ const [email, setEmail] = useState2("");
809
+ const [password, setPassword] = useState2("");
810
+ const [signingIn, setSigningIn] = useState2(false);
811
+ const [history, setHistory] = useState2({ checkpoints: [], published: [] });
812
+ const [message, setMessage] = useState2("");
813
+ const [sending, setSending] = useState2(false);
814
+ const [deletingCheckpointId, setDeletingCheckpointId] = useState2(null);
815
+ const [messages, setMessages] = useState2([]);
816
+ const [assistantAvatarFailed, setAssistantAvatarFailed] = useState2(false);
817
+ const chatEndRef = useRef(null);
818
+ const overlayRootRef = useRef(null);
819
+ const pendingAssistantIdRef = useRef(null);
820
+ const supabase = useMemo2(() => getSupabaseBrowserClient(config), [config]);
821
+ const refreshHistory = useCallback(
822
+ async (showErrorMessage) => {
823
+ if (!token) {
824
+ return;
825
+ }
826
+ try {
827
+ const data = await fetchHistory(config.apiBaseUrl, token);
828
+ setHistory(data);
829
+ } catch {
830
+ if (showErrorMessage) {
831
+ setMessages((prev) => [
832
+ ...prev,
833
+ createMessage("system", "Failed to load rollback history.")
834
+ ]);
835
+ }
836
+ }
837
+ },
838
+ [config.apiBaseUrl, token]
839
+ );
840
+ useEffect2(() => {
841
+ if (!isOpen) {
842
+ return;
843
+ }
844
+ chatEndRef.current?.scrollIntoView({
845
+ behavior: "smooth",
846
+ block: "end"
847
+ });
848
+ }, [isOpen, messages]);
849
+ useEffect2(() => {
850
+ setAssistantAvatarFailed(false);
851
+ }, [config.assistantAvatarUrl]);
852
+ useEffect2(() => {
853
+ if (!isOpen || !isAuthenticated || !token) {
854
+ return;
855
+ }
856
+ void refreshHistory(true);
857
+ }, [isAuthenticated, isOpen, refreshHistory, token]);
858
+ useEffect2(() => {
859
+ if (!isAdminMode || !isOpen) {
860
+ return;
861
+ }
862
+ const onDocumentClickCapture = (event) => {
863
+ const overlayRoot = overlayRootRef.current;
864
+ if (overlayRoot && event.target instanceof Node && overlayRoot.contains(event.target)) {
865
+ return;
866
+ }
867
+ const nextSelection = parseSelectedEditableFromTarget(
868
+ event.target,
869
+ window.location.pathname
870
+ );
871
+ if (!nextSelection) {
872
+ return;
873
+ }
874
+ setSelectedElement(nextSelection);
875
+ };
876
+ document.addEventListener("click", onDocumentClickCapture, true);
877
+ return () => {
878
+ document.removeEventListener("click", onDocumentClickCapture, true);
879
+ };
880
+ }, [isAdminMode, isOpen, setSelectedElement]);
881
+ const selectableModels = modelOptions;
882
+ if (!isAdminMode) {
883
+ return null;
884
+ }
885
+ const signInWithPassword = async () => {
886
+ if (!supabase) {
887
+ setMessages((prev) => [
888
+ ...prev,
889
+ createMessage("system", "Supabase is not configured in frontend env.")
890
+ ]);
891
+ return;
892
+ }
893
+ if (!email.trim() || !password) {
894
+ setMessages((prev) => [
895
+ ...prev,
896
+ createMessage("system", "Email and password are required.")
897
+ ]);
898
+ return;
899
+ }
900
+ setSigningIn(true);
901
+ try {
902
+ const response = await supabase.auth.signInWithPassword({
903
+ email: email.trim(),
904
+ password
905
+ });
906
+ if (response.error) {
907
+ setMessages((prev) => [
908
+ ...prev,
909
+ createMessage("system", `Auth error: ${response.error.message}`)
910
+ ]);
911
+ return;
912
+ }
913
+ setPassword("");
914
+ setMessages((prev) => [...prev, createMessage("system", "Signed in successfully.")]);
915
+ } finally {
916
+ setSigningIn(false);
917
+ }
918
+ };
919
+ const onSend = async () => {
920
+ if (!token || !message.trim() || sending) {
921
+ return;
922
+ }
923
+ const userText = message.trim();
924
+ const userMessage = createMessage("user", userText);
925
+ const pendingAssistantMessage = createMessage("assistant", "", "pending");
926
+ pendingAssistantIdRef.current = pendingAssistantMessage.id;
927
+ setMessage("");
928
+ setSending(true);
929
+ setMessages((prev) => [...prev, userMessage, pendingAssistantMessage]);
930
+ let assistantMessageReceived = false;
931
+ let streamErrorReceived = false;
932
+ try {
933
+ await streamChat({
934
+ apiBaseUrl: config.apiBaseUrl,
935
+ token,
936
+ message: userText,
937
+ modelId: modelId ?? void 0,
938
+ includeThinking,
939
+ currentPath: window.location.pathname,
940
+ selectedElement,
941
+ history: buildModelHistory(messages),
942
+ onEvent: (event) => {
943
+ if (event.event === "thinking") {
944
+ if (!includeThinking) {
945
+ return;
946
+ }
947
+ const note = typeof event.data === "object" && event.data !== null && "note" in event.data ? String(event.data.note) : JSON.stringify(event.data);
948
+ setMessages(
949
+ (prev) => insertBeforePendingMessage(
950
+ prev,
951
+ createMessage("thinking", note),
952
+ pendingAssistantIdRef.current
953
+ )
954
+ );
955
+ return;
956
+ }
957
+ if (event.event === "tool") {
958
+ const toolName = typeof event.data === "object" && event.data !== null && "tool" in event.data ? String(event.data.tool) : "tool";
959
+ const summary = typeof event.data === "object" && event.data !== null && "summary" in event.data ? String(event.data.summary) : "Executed tool step.";
960
+ setMessages(
961
+ (prev) => insertBeforePendingMessage(
962
+ prev,
963
+ createMessage("tool", toReadableToolLine(toolName, summary)),
964
+ pendingAssistantIdRef.current
965
+ )
966
+ );
967
+ return;
968
+ }
969
+ if (event.event === "message") {
970
+ const text = typeof event.data === "object" && event.data !== null && "text" in event.data ? String(event.data.text) : String(event.data);
971
+ const normalizedText = text.trim();
972
+ if (!normalizedText) {
973
+ return;
974
+ }
975
+ assistantMessageReceived = true;
976
+ const pendingAssistantId = pendingAssistantIdRef.current;
977
+ setMessages((prev) => {
978
+ const { nextEntries, replaced } = resolvePendingAssistant(
979
+ prev,
980
+ pendingAssistantId,
981
+ normalizedText
982
+ );
983
+ if (replaced) {
984
+ return nextEntries;
985
+ }
986
+ return [...prev, createMessage("assistant", normalizedText, "final")];
987
+ });
988
+ pendingAssistantIdRef.current = null;
989
+ return;
990
+ }
991
+ if (event.event === "done") {
992
+ if (!assistantMessageReceived && !streamErrorReceived) {
993
+ const pendingAssistantId = pendingAssistantIdRef.current;
994
+ setMessages((prev) => [
995
+ ...removeMessageById(prev, pendingAssistantId),
996
+ createMessage("system", "No assistant response received. Please retry.")
997
+ ]);
998
+ pendingAssistantIdRef.current = null;
999
+ }
1000
+ return;
1001
+ }
1002
+ if (event.event === "draft-updated") {
1003
+ requestRefresh();
1004
+ void refreshHistory(false);
1005
+ return;
1006
+ }
1007
+ if (event.event === "error") {
1008
+ const detail = typeof event.data === "object" && event.data !== null && "error" in event.data ? String(event.data.error) : "Unknown stream error.";
1009
+ const pendingAssistantId = pendingAssistantIdRef.current;
1010
+ setMessages((prev) => [
1011
+ ...removeMessageById(prev, pendingAssistantId),
1012
+ createMessage("system", `**Error:** ${detail}`)
1013
+ ]);
1014
+ pendingAssistantIdRef.current = null;
1015
+ streamErrorReceived = true;
1016
+ return;
1017
+ }
1018
+ }
1019
+ });
1020
+ } catch (error) {
1021
+ const detail = error instanceof Error ? error.message : "Chat failed.";
1022
+ const pendingAssistantId = pendingAssistantIdRef.current;
1023
+ setMessages((prev) => [...removeMessageById(prev, pendingAssistantId), createMessage("system", detail)]);
1024
+ pendingAssistantIdRef.current = null;
1025
+ } finally {
1026
+ pendingAssistantIdRef.current = null;
1027
+ setSending(false);
1028
+ }
1029
+ };
1030
+ const onPublish = async () => {
1031
+ if (!token) {
1032
+ return;
1033
+ }
1034
+ const approved = window.confirm(
1035
+ "Publish current draft to live site? This action affects all visitors."
1036
+ );
1037
+ if (!approved) {
1038
+ return;
1039
+ }
1040
+ try {
1041
+ await publishDraft(config.apiBaseUrl, token, {
1042
+ confirmationText: REQUIRED_PUBLISH_CONFIRMATION
1043
+ });
1044
+ requestRefresh();
1045
+ await refreshHistory(false);
1046
+ setMessages((prev) => [
1047
+ ...prev,
1048
+ createMessage("system", "Draft published successfully.")
1049
+ ]);
1050
+ } catch (error) {
1051
+ const detail = error instanceof Error ? error.message : "Publish failed.";
1052
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1053
+ }
1054
+ };
1055
+ const onRollback = async (request, label) => {
1056
+ if (!token) {
1057
+ return;
1058
+ }
1059
+ try {
1060
+ await rollbackDraft(config.apiBaseUrl, token, request);
1061
+ requestRefresh();
1062
+ await refreshHistory(false);
1063
+ setMessages((prev) => [
1064
+ ...prev,
1065
+ createMessage("system", `Draft restored from ${label}.`)
1066
+ ]);
1067
+ } catch (error) {
1068
+ const detail = error instanceof Error ? error.message : "Rollback failed.";
1069
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1070
+ }
1071
+ };
1072
+ const onDeleteCheckpoint = async (checkpoint) => {
1073
+ if (!token || deletingCheckpointId) {
1074
+ return;
1075
+ }
1076
+ const timestampLabel = formatHistoryTime(checkpoint.createdAt);
1077
+ const reasonLine = checkpoint.reason ? `
1078
+ Reason: ${checkpoint.reason}` : "";
1079
+ const approved = window.confirm(
1080
+ `Delete checkpoint from ${timestampLabel}? This cannot be undone.${reasonLine}`
1081
+ );
1082
+ if (!approved) {
1083
+ return;
1084
+ }
1085
+ setDeletingCheckpointId(checkpoint.id);
1086
+ try {
1087
+ await deleteCheckpoint(config.apiBaseUrl, token, { checkpointId: checkpoint.id });
1088
+ await refreshHistory(false);
1089
+ setMessages((prev) => [
1090
+ ...prev,
1091
+ createMessage("system", `Deleted checkpoint from ${timestampLabel}.`)
1092
+ ]);
1093
+ } catch (error) {
1094
+ const detail = error instanceof Error ? error.message : "Delete checkpoint failed.";
1095
+ setMessages((prev) => [...prev, createMessage("system", detail)]);
1096
+ } finally {
1097
+ setDeletingCheckpointId((current) => current === checkpoint.id ? null : current);
1098
+ }
1099
+ };
1100
+ const onClearChat = () => {
1101
+ if (sending) {
1102
+ return;
1103
+ }
1104
+ if (messages.length === 0 && !message.trim() && !selectedElement) {
1105
+ return;
1106
+ }
1107
+ pendingAssistantIdRef.current = null;
1108
+ setMessages([]);
1109
+ setMessage("");
1110
+ clearSelectedElement();
1111
+ };
1112
+ const onMessageKeyDown = (event) => {
1113
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) {
1114
+ return;
1115
+ }
1116
+ event.preventDefault();
1117
+ if (!isAuthenticated || sending || !message.trim()) {
1118
+ return;
1119
+ }
1120
+ void onSend();
1121
+ };
1122
+ const latestPublished = history.published.reduce((max, item) => {
1123
+ const value = historyTimestamp(item.createdAt);
1124
+ if (value === null) {
1125
+ return max;
1126
+ }
1127
+ return max === null ? value : Math.max(max, value);
1128
+ }, null);
1129
+ const latestCheckpoint = history.checkpoints.reduce((max, item) => {
1130
+ const value = historyTimestamp(item.createdAt);
1131
+ if (value === null) {
1132
+ return max;
1133
+ }
1134
+ return max === null ? value : Math.max(max, value);
1135
+ }, null);
1136
+ const publishState = latestCheckpoint !== null && (latestPublished === null || latestCheckpoint > latestPublished) ? "Unpublished" : "Published";
1137
+ const assistantAvatarFallbackLabel = (config.assistantAvatarFallback || "W").trim().charAt(0).toUpperCase() || "W";
1138
+ const showAssistantAvatarImage = Boolean(config.assistantAvatarUrl) && !assistantAvatarFailed;
1139
+ return /* @__PURE__ */ jsx3(Fragment, { children: isOpen ? /* @__PURE__ */ jsxs(
1140
+ "div",
1141
+ {
1142
+ ref: overlayRootRef,
1143
+ "data-admin-overlay-root": true,
1144
+ className: "fixed bottom-4 right-4 z-[100] flex h-[62vh] w-[min(480px,calc(100vw-1.5rem))] flex-col overflow-hidden rounded-lg border border-stone-300 bg-[#f6f2eb] text-stone-900 shadow-2xl",
1145
+ style: {
1146
+ fontFamily: "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
1147
+ },
1148
+ children: [
1149
+ /* @__PURE__ */ jsx3("header", { className: "border-b border-stone-300 bg-[#f3eee5] p-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1150
+ isAuthenticated ? /* @__PURE__ */ jsxs(Fragment, { children: [
1151
+ /* @__PURE__ */ jsx3(
1152
+ "span",
1153
+ {
1154
+ className: `rounded border px-1.5 py-0.5 text-[10px] font-medium leading-4 ${publishState === "Published" ? "border-stone-300 bg-[#ece5d9] text-stone-600" : "border-stone-500 bg-[#ded4c3] text-stone-800"}`,
1155
+ children: publishState
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsx3(
1159
+ "button",
1160
+ {
1161
+ type: "button",
1162
+ className: "rounded border border-stone-700 bg-stone-800 px-2 py-1 text-[11px] font-semibold leading-4 text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1163
+ onClick: onPublish,
1164
+ disabled: !isAuthenticated,
1165
+ children: "Publish"
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ jsxs("div", { className: "inline-flex rounded-md border border-stone-300 bg-[#e8dfd1] p-0.5", children: [
1169
+ /* @__PURE__ */ jsx3(
1170
+ "button",
1171
+ {
1172
+ type: "button",
1173
+ className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "chat" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
1174
+ onClick: () => setActiveTab("chat"),
1175
+ children: "Chat"
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ jsxs(
1179
+ "button",
1180
+ {
1181
+ type: "button",
1182
+ className: `rounded px-2 py-1 text-[11px] font-medium leading-4 ${activeTab === "history" ? "bg-[#f7f2e8] text-stone-900 shadow-sm" : "text-stone-600 hover:text-stone-900"}`,
1183
+ onClick: () => setActiveTab("history"),
1184
+ children: [
1185
+ "History (",
1186
+ history.published.length + history.checkpoints.length,
1187
+ ")"
1188
+ ]
1189
+ }
1190
+ )
1191
+ ] })
1192
+ ] }) : /* @__PURE__ */ jsx3("h2", { className: "text-[12px] font-semibold text-stone-700", children: "Login" }),
1193
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
1194
+ isAuthenticated ? /* @__PURE__ */ jsx3(
1195
+ "button",
1196
+ {
1197
+ type: "button",
1198
+ "aria-label": "Clear chat",
1199
+ title: "Clear chat",
1200
+ disabled: sending || messages.length === 0 && !message.trim() && !selectedElement,
1201
+ className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 text-stone-600 hover:bg-[#efe8dc] hover:text-stone-800 disabled:cursor-not-allowed disabled:opacity-50",
1202
+ onClick: onClearChat,
1203
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1204
+ "path",
1205
+ {
1206
+ d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
1207
+ stroke: "currentColor",
1208
+ strokeWidth: "1.4",
1209
+ strokeLinecap: "round",
1210
+ strokeLinejoin: "round"
1211
+ }
1212
+ ) })
1213
+ }
1214
+ ) : null,
1215
+ /* @__PURE__ */ jsx3(
1216
+ "button",
1217
+ {
1218
+ type: "button",
1219
+ className: "rounded border border-stone-300 px-2 py-1 text-[11px] leading-4 text-stone-700 hover:bg-[#efe8dc]",
1220
+ onClick: () => setIsOpen(false),
1221
+ children: "Close"
1222
+ }
1223
+ )
1224
+ ] })
1225
+ ] }) }),
1226
+ !isAuthenticated ? /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 items-center justify-center bg-[#ece7dd] p-3", children: !authConfigured ? /* @__PURE__ */ jsx3("div", { className: "w-full max-w-sm rounded border border-red-300 bg-[#f8f3e9] p-3 text-[11px] leading-4 text-red-700", children: "Missing Supabase config (`supabaseUrl` / `supabaseAnonKey`)." }) : /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded border border-stone-300 bg-[#f8f3e9] p-3", children: [
1227
+ /* @__PURE__ */ jsx3("h3", { className: "mb-2 text-[12px] font-semibold text-stone-700", children: "Sign in" }),
1228
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1229
+ /* @__PURE__ */ jsx3(
1230
+ "input",
1231
+ {
1232
+ type: "text",
1233
+ value: email,
1234
+ onChange: (event) => setEmail(event.target.value),
1235
+ placeholder: "login",
1236
+ className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsx3(
1240
+ "input",
1241
+ {
1242
+ type: "password",
1243
+ value: password,
1244
+ onChange: (event) => setPassword(event.target.value),
1245
+ placeholder: "Password",
1246
+ className: "w-full rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] text-stone-900 outline-none focus:border-stone-500"
1247
+ }
1248
+ ),
1249
+ /* @__PURE__ */ jsx3(
1250
+ "button",
1251
+ {
1252
+ type: "button",
1253
+ onClick: signInWithPassword,
1254
+ disabled: signingIn || !email.trim() || !password,
1255
+ className: "w-full rounded border border-stone-700 bg-stone-800 px-2 py-1.5 text-[12px] font-medium text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1256
+ children: signingIn ? "Signing in" : "Sign in"
1257
+ }
1258
+ )
1259
+ ] })
1260
+ ] }) }) : activeTab === "chat" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1261
+ /* @__PURE__ */ jsxs("section", { className: "flex-1 space-y-1 overflow-auto bg-[#ece7dd] p-2", children: [
1262
+ messages.map((entry) => {
1263
+ const isAssistant = entry.role === "assistant";
1264
+ const isPendingAssistant = isAssistant && entry.status === "pending";
1265
+ return /* @__PURE__ */ jsx3(
1266
+ "div",
1267
+ {
1268
+ className: entry.role === "tool" ? "max-w-[96%] px-0.5 py-0 text-[10px] leading-tight text-stone-500" : `max-w-[92%] rounded-md py-1.5 text-[12px] leading-4 ${entry.role === "user" ? "ml-auto bg-[#2e2b27] px-2 text-stone-50" : entry.role === "thinking" ? "bg-[#e3dbce] px-2 text-stone-700" : isAssistant ? "relative border border-[#d6ccbb] bg-[#f8f3e9] pl-8 pr-2 text-stone-800" : "bg-[#ddd2bf] px-2 text-stone-800"}`,
1269
+ children: entry.role === "tool" ? /* @__PURE__ */ jsx3("span", { children: entry.text }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1270
+ isAssistant ? showAssistantAvatarImage ? /* @__PURE__ */ jsx3(
1271
+ "img",
1272
+ {
1273
+ src: config.assistantAvatarUrl,
1274
+ alt: "",
1275
+ "aria-hidden": "true",
1276
+ className: `pointer-events-none absolute left-2 top-1.5 h-[18px] w-[18px] select-none rounded-full border border-[#d6ccbb] bg-[#efe8dc] object-cover ${isPendingAssistant ? "animate-pulse" : ""}`,
1277
+ onError: () => setAssistantAvatarFailed(true)
1278
+ }
1279
+ ) : /* @__PURE__ */ jsx3(
1280
+ "span",
1281
+ {
1282
+ "aria-hidden": "true",
1283
+ className: `pointer-events-none absolute left-2 top-1.5 inline-flex h-[18px] w-[18px] select-none items-center justify-center rounded-full border border-[#d6ccbb] bg-[#efe8dc] text-[9px] font-semibold text-stone-700 ${isPendingAssistant ? "animate-pulse" : ""}`,
1284
+ children: assistantAvatarFallbackLabel
1285
+ }
1286
+ ) : null,
1287
+ /* @__PURE__ */ jsx3("div", { className: "max-w-none text-inherit [&_code]:rounded [&_code]:bg-stone-900/10 [&_code]:px-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_p]:mb-1 [&_p:last-child]:mb-0 [&_ul]:list-disc [&_ul]:pl-4", children: isPendingAssistant && !entry.text.trim() ? /* @__PURE__ */ jsx3("span", { className: "block h-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx3(ReactMarkdown, { remarkPlugins: [remarkGfm], children: entry.text }) })
1288
+ ] })
1289
+ },
1290
+ entry.id
1291
+ );
1292
+ }),
1293
+ /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
1294
+ ] }),
1295
+ /* @__PURE__ */ jsxs("footer", { className: "border-t border-stone-300 bg-[#f3eee5] p-2", children: [
1296
+ showModelPicker && selectableModels.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
1297
+ /* @__PURE__ */ jsx3(
1298
+ "label",
1299
+ {
1300
+ htmlFor: "admin-model-picker",
1301
+ className: "text-[10px] font-semibold uppercase tracking-wide text-stone-600",
1302
+ children: "Model"
1303
+ }
1304
+ ),
1305
+ /* @__PURE__ */ jsx3(
1306
+ "select",
1307
+ {
1308
+ id: "admin-model-picker",
1309
+ value: modelId ?? selectableModels[0]?.id,
1310
+ onChange: (event) => setModelId(event.target.value),
1311
+ disabled: sending,
1312
+ className: "h-7 min-w-0 flex-1 rounded border border-stone-300 bg-[#f7f2e8] px-2 text-[11px] text-stone-800 outline-none focus:border-stone-500 disabled:cursor-not-allowed disabled:opacity-60",
1313
+ children: selectableModels.map((option) => /* @__PURE__ */ jsx3("option", { value: option.id, children: option.label }, option.id))
1314
+ }
1315
+ )
1316
+ ] }) : null,
1317
+ selectedElement ? /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1 rounded border border-stone-300 bg-[#e8dfd1] px-1.5 py-1", children: [
1318
+ /* @__PURE__ */ jsx3("span", { className: "inline-flex shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] px-1 py-0.5 text-[9px] font-semibold text-stone-700", children: kindIcon(selectedElement.kind) }),
1319
+ /* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-[10px] leading-3.5 text-stone-600", children: [
1320
+ /* @__PURE__ */ jsx3("span", { className: "font-semibold text-stone-800", children: selectedElement.label }),
1321
+ /* @__PURE__ */ jsxs("span", { children: [
1322
+ " \xB7 ",
1323
+ selectedElement.path
1324
+ ] }),
1325
+ selectedElement.preview ? /* @__PURE__ */ jsxs("span", { children: [
1326
+ " \xB7 ",
1327
+ selectedElement.preview
1328
+ ] }) : null
1329
+ ] }),
1330
+ /* @__PURE__ */ jsx3(
1331
+ "button",
1332
+ {
1333
+ type: "button",
1334
+ "aria-label": "Clear selected element",
1335
+ title: "Clear selected element",
1336
+ className: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc]",
1337
+ onClick: clearSelectedElement,
1338
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1339
+ "path",
1340
+ {
1341
+ d: "M5 5L15 15M15 5L5 15",
1342
+ stroke: "currentColor",
1343
+ strokeWidth: "1.5",
1344
+ strokeLinecap: "round"
1345
+ }
1346
+ ) })
1347
+ }
1348
+ )
1349
+ ] }) : null,
1350
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1351
+ /* @__PURE__ */ jsx3(
1352
+ "textarea",
1353
+ {
1354
+ value: message,
1355
+ onChange: (event) => setMessage(event.target.value),
1356
+ onKeyDown: onMessageKeyDown,
1357
+ rows: 2,
1358
+ placeholder: "Ask the agent to edit text, image URLs, or theme tokens",
1359
+ className: "flex-1 resize-none rounded border border-stone-300 bg-[#f4efe6] px-2 py-1.5 text-[12px] leading-4 text-stone-900 outline-none placeholder:text-stone-500 focus:border-stone-500"
1360
+ }
1361
+ ),
1362
+ /* @__PURE__ */ jsx3(
1363
+ "button",
1364
+ {
1365
+ type: "button",
1366
+ onClick: onSend,
1367
+ disabled: !isAuthenticated || sending || !message.trim(),
1368
+ className: "rounded border border-stone-500 bg-stone-600 px-3 py-1.5 text-[12px] font-semibold text-stone-100 hover:bg-stone-700 disabled:cursor-not-allowed disabled:opacity-50",
1369
+ children: sending ? "Sending" : "Send"
1370
+ }
1371
+ )
1372
+ ] })
1373
+ ] })
1374
+ ] }) : /* @__PURE__ */ jsx3("section", { className: "flex min-h-0 flex-1 flex-col p-2 text-[11px] leading-4", children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden", children: [
1375
+ /* @__PURE__ */ jsxs("div", { className: "rounded border border-stone-300 bg-[#f8f3e9]", children: [
1376
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1377
+ "Published (",
1378
+ history.published.length,
1379
+ ")"
1380
+ ] }),
1381
+ /* @__PURE__ */ jsx3("div", { className: "max-h-40 overflow-auto px-2 py-1.5", children: history.published.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.published.map((item) => /* @__PURE__ */ jsxs(
1382
+ "div",
1383
+ {
1384
+ className: "flex items-center justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1385
+ children: [
1386
+ /* @__PURE__ */ jsx3("span", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1387
+ /* @__PURE__ */ jsx3(
1388
+ "button",
1389
+ {
1390
+ type: "button",
1391
+ className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc]",
1392
+ onClick: () => onRollback(
1393
+ { sourceType: "published", sourceId: item.id },
1394
+ `published snapshot at ${formatHistoryTime(item.createdAt)}`
1395
+ ),
1396
+ children: "Restore"
1397
+ }
1398
+ )
1399
+ ]
1400
+ },
1401
+ `pub-${item.id}`
1402
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No published snapshots." }) })
1403
+ ] }),
1404
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col rounded border border-stone-300 bg-[#f8f3e9]", children: [
1405
+ /* @__PURE__ */ jsxs("div", { className: "border-b border-stone-200 px-2 py-1 font-semibold text-stone-700", children: [
1406
+ "Checkpoints (",
1407
+ history.checkpoints.length,
1408
+ ")"
1409
+ ] }),
1410
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-auto px-2 py-1.5", children: history.checkpoints.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "space-y-1", children: history.checkpoints.map((item) => /* @__PURE__ */ jsxs(
1411
+ "div",
1412
+ {
1413
+ className: "flex items-start justify-between gap-2 rounded border border-stone-200 bg-[#f2ecdf] px-2 py-1",
1414
+ children: [
1415
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1416
+ /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-700", children: formatHistoryTime(item.createdAt) }),
1417
+ item.reason ? /* @__PURE__ */ jsx3("p", { className: "truncate text-[10px] text-stone-500", children: item.reason }) : null
1418
+ ] }),
1419
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1420
+ /* @__PURE__ */ jsx3(
1421
+ "button",
1422
+ {
1423
+ type: "button",
1424
+ disabled: deletingCheckpointId === item.id,
1425
+ className: "rounded border border-stone-300 bg-[#f7f2e8] px-1.5 py-0.5 text-[10px] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1426
+ onClick: () => onRollback(
1427
+ { sourceType: "checkpoint", sourceId: item.id },
1428
+ `checkpoint at ${formatHistoryTime(item.createdAt)}`
1429
+ ),
1430
+ children: "Restore"
1431
+ }
1432
+ ),
1433
+ /* @__PURE__ */ jsx3(
1434
+ "button",
1435
+ {
1436
+ type: "button",
1437
+ "aria-label": "Delete checkpoint",
1438
+ title: "Delete checkpoint",
1439
+ disabled: deletingCheckpointId === item.id,
1440
+ className: "inline-flex h-6 w-6 items-center justify-center rounded border border-stone-300 bg-[#f7f2e8] text-stone-700 hover:bg-[#efe8dc] disabled:cursor-not-allowed disabled:opacity-50",
1441
+ onClick: () => {
1442
+ void onDeleteCheckpoint(item);
1443
+ },
1444
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-3.5 w-3.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
1445
+ "path",
1446
+ {
1447
+ d: "M4.5 5.5H15.5M8 3.75H12M7 7.5V13.5M10 7.5V13.5M13 7.5V13.5M6.5 5.5L7 15C7.03 15.6 7.53 16.08 8.13 16.08H11.87C12.47 16.08 12.97 15.6 13 15L13.5 5.5",
1448
+ stroke: "currentColor",
1449
+ strokeWidth: "1.4",
1450
+ strokeLinecap: "round",
1451
+ strokeLinejoin: "round"
1452
+ }
1453
+ ) })
1454
+ }
1455
+ )
1456
+ ] })
1457
+ ]
1458
+ },
1459
+ `cp-${item.id}`
1460
+ )) }) : /* @__PURE__ */ jsx3("p", { className: "text-[10px] text-stone-500", children: "No checkpoints yet." }) })
1461
+ ] })
1462
+ ] }) })
1463
+ ]
1464
+ }
1465
+ ) : /* @__PURE__ */ jsx3(
1466
+ "button",
1467
+ {
1468
+ type: "button",
1469
+ onClick: () => setIsOpen(true),
1470
+ className: "fixed bottom-4 right-4 z-[100] rounded-full border border-stone-600 bg-stone-700 px-4 py-2 text-[12px] font-semibold text-stone-100 shadow-xl hover:bg-stone-800",
1471
+ style: {
1472
+ fontFamily: "var(--font-ibm-plex-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
1473
+ },
1474
+ children: "Chat to Webmaster"
1475
+ }
1476
+ ) });
1477
+ }
1478
+
1479
+ // src/runtime.tsx
1480
+ import {
1481
+ createContext as createContext3,
1482
+ useContext as useContext3,
1483
+ useEffect as useEffect3,
1484
+ useMemo as useMemo3,
1485
+ useState as useState3
1486
+ } from "react";
1487
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1488
+ var CmsRuntimeContext = createContext3(null);
1489
+ function createThemeCssVariables(tokens) {
1490
+ return {
1491
+ ["--brand-primary"]: tokens.brandPrimary,
1492
+ ["--brand-primary-dark"]: tokens.brandPrimaryDark,
1493
+ ["--brand-primary-light"]: tokens.brandPrimaryLight,
1494
+ ["--brand-dark"]: tokens.brandDark,
1495
+ ["--brand-text"]: tokens.brandText,
1496
+ ["--brand-surface"]: tokens.brandSurface,
1497
+ ["--brand-border"]: tokens.brandBorder
1498
+ };
1499
+ }
1500
+ function CmsRuntimeBridge(props) {
1501
+ const { config, isAdminMode, isAuthenticated, token, refreshKey } = useWebmasterDroid();
1502
+ const stage = useMemo3(
1503
+ () => isAdminMode && isAuthenticated ? "draft" : "live",
1504
+ [isAdminMode, isAuthenticated]
1505
+ );
1506
+ const requestKey = useMemo3(
1507
+ () => `${stage}:${token ?? "anon"}:${refreshKey}`,
1508
+ [refreshKey, stage, token]
1509
+ );
1510
+ const [state, setState] = useState3({
1511
+ requestKey: "",
1512
+ document: props.fallbackDocument,
1513
+ error: null
1514
+ });
1515
+ useEffect3(() => {
1516
+ let ignore = false;
1517
+ fetchCmsContent(config.apiBaseUrl, stage, token).then((content2) => {
1518
+ if (ignore) {
1519
+ return;
1520
+ }
1521
+ setState({
1522
+ requestKey,
1523
+ document: content2,
1524
+ error: null
1525
+ });
1526
+ }).catch((error2) => {
1527
+ if (ignore) {
1528
+ return;
1529
+ }
1530
+ const message = error2 instanceof Error ? error2.message : "Failed to load content.";
1531
+ setState({
1532
+ requestKey,
1533
+ document: props.fallbackDocument,
1534
+ error: message
1535
+ });
1536
+ });
1537
+ return () => {
1538
+ ignore = true;
1539
+ };
1540
+ }, [config.apiBaseUrl, props.fallbackDocument, requestKey, stage, token]);
1541
+ const loading = state.requestKey !== requestKey;
1542
+ const error = loading ? null : state.error;
1543
+ const value = useMemo3(
1544
+ () => ({
1545
+ document: state.document,
1546
+ stage,
1547
+ loading,
1548
+ error
1549
+ }),
1550
+ [error, loading, stage, state.document]
1551
+ );
1552
+ const content = props.applyThemeTokens ? /* @__PURE__ */ jsx4("div", { style: createThemeCssVariables(value.document.themeTokens), children: props.children }) : props.children;
1553
+ return /* @__PURE__ */ jsxs2(CmsRuntimeContext.Provider, { value, children: [
1554
+ /* @__PURE__ */ jsx4(EditableProvider, { document: value.document, mode: stage, enabled: isAdminMode, children: content }),
1555
+ props.includeOverlay ? /* @__PURE__ */ jsx4(WebmasterDroidOverlay, {}) : null
1556
+ ] });
1557
+ }
1558
+ function WebmasterDroidRuntime(props) {
1559
+ return /* @__PURE__ */ jsx4(WebmasterDroidProvider, { config: props.config, children: /* @__PURE__ */ jsx4(
1560
+ CmsRuntimeBridge,
1561
+ {
1562
+ fallbackDocument: props.fallbackDocument,
1563
+ includeOverlay: props.includeOverlay ?? true,
1564
+ applyThemeTokens: props.applyThemeTokens ?? true,
1565
+ children: props.children
1566
+ }
1567
+ ) });
1568
+ }
1569
+ function useWebmasterDroidCmsDocument() {
1570
+ const context = useContext3(CmsRuntimeContext);
1571
+ if (!context) {
1572
+ throw new Error("useWebmasterDroidCmsDocument must be used within <WebmasterDroidRuntime>");
1573
+ }
1574
+ return context;
1575
+ }
1576
+ export {
1577
+ EditableImage,
1578
+ EditableLink,
1579
+ EditableProvider,
1580
+ EditableRichText,
1581
+ EditableText,
1582
+ WebmasterDroidOverlay,
1583
+ WebmasterDroidProvider,
1584
+ WebmasterDroidRuntime,
1585
+ buildApiUrl,
1586
+ deleteCheckpoint,
1587
+ editableMeta,
1588
+ fetchCmsContent,
1589
+ fetchHistory,
1590
+ fetchModels,
1591
+ getSupabaseBrowserClient,
1592
+ parseSelectedEditableFromTarget,
1593
+ publishDraft,
1594
+ resolveWebmasterDroidConfig,
1595
+ rollbackDraft,
1596
+ streamChat,
1597
+ useEditableDocument,
1598
+ useWebmasterDroid,
1599
+ useWebmasterDroidCmsDocument
1600
+ };