@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.d.ts +742 -0
- package/dist/index.js +1600 -0
- package/dist/styles.css +2 -0
- package/package.json +53 -0
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
|
+
};
|