@joai/core 4.15.7 → 5.0.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.
@@ -1,729 +0,0 @@
1
- 'use client';
2
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/react/guest.ts
5
- function safeGet(key) {
6
- try {
7
- return localStorage.getItem(key);
8
- } catch {
9
- return null;
10
- }
11
- }
12
- function safeSet(key, value) {
13
- try {
14
- localStorage.setItem(key, value);
15
- } catch {}
16
- }
17
- function safeRemove(key) {
18
- try {
19
- localStorage.removeItem(key);
20
- } catch {}
21
- }
22
- function getOrCreateGuestId(storageKey = "joai_embed_guest") {
23
- if (typeof window === "undefined") return "";
24
- const existing = safeGet(storageKey);
25
- if (existing) return existing;
26
- const id = crypto.randomUUID();
27
- safeSet(storageKey, id);
28
- return id;
29
- }
30
- function loadRoom(storageKey) {
31
- return safeGet(storageKey);
32
- }
33
- function saveRoom(storageKey, roomId) {
34
- safeSet(storageKey, roomId);
35
- }
36
- function clearRoom(storageKey) {
37
- safeRemove(storageKey);
38
- }
39
- //#endregion
40
- //#region src/react/stream-connection.ts
41
- function openEventSourceStream(url, onData, options) {
42
- const es = new EventSource(url, { withCredentials: options?.withCredentials ?? false });
43
- es.onmessage = (event) => {
44
- try {
45
- onData(JSON.parse(event.data));
46
- } catch (e) {
47
- options?.onError?.(e);
48
- }
49
- };
50
- es.onerror = (event) => {
51
- options?.onConnectionError?.(event);
52
- };
53
- return () => es.close();
54
- }
55
- //#endregion
56
- //#region src/react/components.tsx
57
- function EmbedChatList({ items, agentName, awaitingResponse, classNames = {}, renderEmptyState, renderAgentContent }) {
58
- const containerRef = useRef(null);
59
- useEffect(() => {
60
- const el = containerRef.current;
61
- if (!el) return;
62
- if (el.scrollHeight - el.scrollTop - el.clientHeight < 120) el.scrollTop = el.scrollHeight;
63
- }, [items, awaitingResponse]);
64
- if (items.length === 0 && !awaitingResponse) return renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", {
65
- className: classNames.container,
66
- children: /* @__PURE__ */ jsxs("p", { children: [
67
- "Send ",
68
- agentName,
69
- " a message to start chatting"
70
- ] })
71
- });
72
- return /* @__PURE__ */ jsxs("div", {
73
- ref: containerRef,
74
- className: classNames.container,
75
- role: "log",
76
- "aria-live": "polite",
77
- children: [items.map((item, i) => /* @__PURE__ */ jsx(EmbedChatBubble, {
78
- item,
79
- classNames,
80
- renderAgentContent
81
- }, i)), awaitingResponse && /* @__PURE__ */ jsx(EmbedThinkingIndicator, {
82
- wrapperClassName: classNames.thinkingWrapper,
83
- dotClassName: classNames.dot
84
- })]
85
- });
86
- }
87
- function EmbedChatBubble({ item, classNames, renderAgentContent }) {
88
- if (item.byUser) return /* @__PURE__ */ jsx("div", {
89
- className: classNames.userBubble,
90
- children: item.content
91
- });
92
- return /* @__PURE__ */ jsx("div", {
93
- className: item.error ? classNames.errorBubble : classNames.agentBubble,
94
- children: renderAgentContent ? renderAgentContent(item) : /* @__PURE__ */ jsxs(Fragment, { children: [item.content, item.streaming && /* @__PURE__ */ jsx("span", { className: "inline-block w-1 h-3 ml-1 bg-current animate-pulse rounded-full" })] })
95
- });
96
- }
97
- function EmbedThinkingIndicator({ wrapperClassName, dotClassName }) {
98
- return /* @__PURE__ */ jsx("div", {
99
- className: wrapperClassName,
100
- children: /* @__PURE__ */ jsxs("span", {
101
- className: "inline-flex gap-1",
102
- children: [
103
- /* @__PURE__ */ jsx("span", {
104
- className: dotClassName,
105
- style: { animationDelay: "0ms" }
106
- }),
107
- /* @__PURE__ */ jsx("span", {
108
- className: dotClassName,
109
- style: { animationDelay: "150ms" }
110
- }),
111
- /* @__PURE__ */ jsx("span", {
112
- className: dotClassName,
113
- style: { animationDelay: "300ms" }
114
- })
115
- ]
116
- })
117
- });
118
- }
119
- function EmbedChatInput({ onSend, disabled, placeholder = "Type a message...", maxLength = 4e3, classNames = {}, renderSendButton }) {
120
- const [text, setText] = useState("");
121
- const textareaRef = useRef(null);
122
- function handleSend() {
123
- const trimmed = text.trim();
124
- if (!trimmed || disabled) return;
125
- onSend(trimmed);
126
- setText("");
127
- if (textareaRef.current) textareaRef.current.style.height = "auto";
128
- }
129
- function handleKeyDown(e) {
130
- if (e.key === "Enter" && !e.shiftKey) {
131
- e.preventDefault();
132
- handleSend();
133
- }
134
- }
135
- function handleInput() {
136
- if (textareaRef.current) {
137
- textareaRef.current.style.height = "auto";
138
- textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + "px";
139
- }
140
- }
141
- return /* @__PURE__ */ jsxs("div", {
142
- className: classNames.container,
143
- children: [/* @__PURE__ */ jsx("textarea", {
144
- ref: textareaRef,
145
- value: text,
146
- onChange: (e) => setText(e.target.value),
147
- onKeyDown: handleKeyDown,
148
- onInput: handleInput,
149
- placeholder,
150
- "aria-label": "Message input",
151
- maxLength,
152
- rows: 1,
153
- disabled,
154
- className: classNames.textarea
155
- }), renderSendButton({
156
- onClick: handleSend,
157
- disabled: disabled || !text.trim()
158
- })]
159
- });
160
- }
161
- //#endregion
162
- //#region src/react/use-embed-chat.ts
163
- const RESPONSE_TIMEOUT_MS = 3e4;
164
- function useEmbedChat({ agentUuid, http, cortexBaseUrl, guestId, roomStorageKey, enabled = true, extraPayload }) {
165
- const [items, setItems] = useState([]);
166
- const [sending, setSending] = useState(false);
167
- const [awaitingResponse, setAwaitingResponse] = useState(false);
168
- const [roomId, setRoomId] = useState(null);
169
- const [connectionError, setConnectionError] = useState(false);
170
- const roomIdRef = useRef(null);
171
- roomIdRef.current = roomId;
172
- const extraPayloadRef = useRef(extraPayload);
173
- extraPayloadRef.current = extraPayload;
174
- useEffect(() => {
175
- if (!roomId) return;
176
- setConnectionError(false);
177
- return openEventSourceStream(`${cortexBaseUrl}/agents/stream/${roomId}`, (raw) => {
178
- setConnectionError(false);
179
- const data = raw;
180
- if (data.type === "response:chunk") {
181
- setAwaitingResponse(false);
182
- setItems((prev) => {
183
- const last = prev[prev.length - 1];
184
- if (last && last.streaming && !last.byUser) {
185
- const updated = [...prev];
186
- updated[updated.length - 1] = {
187
- ...last,
188
- content: last.content + (data.content ?? "")
189
- };
190
- return updated;
191
- }
192
- return [...prev, {
193
- content: data.content ?? "",
194
- byUser: false,
195
- streaming: true
196
- }];
197
- });
198
- } else if (data.type === "response") {
199
- setAwaitingResponse(false);
200
- setItems((prev) => {
201
- const updated = [...prev];
202
- const lastIdx = updated.length - 1;
203
- if (lastIdx >= 0 && updated[lastIdx].streaming) updated[lastIdx] = {
204
- ...updated[lastIdx],
205
- content: data.content ?? updated[lastIdx].content,
206
- streaming: false
207
- };
208
- else updated.push({
209
- content: data.content ?? "",
210
- byUser: false
211
- });
212
- return updated;
213
- });
214
- } else if (data.type === "complete") {
215
- setAwaitingResponse(false);
216
- setItems((prev) => prev.map((item) => item.streaming ? {
217
- ...item,
218
- streaming: false
219
- } : item));
220
- }
221
- }, { onConnectionError: () => setConnectionError(true) });
222
- }, [roomId, cortexBaseUrl]);
223
- useEffect(() => {
224
- if (!awaitingResponse) return;
225
- const timer = setTimeout(() => {
226
- setAwaitingResponse(false);
227
- setItems((prev) => [...prev, {
228
- content: "Response timed out. Please try again.",
229
- byUser: false,
230
- error: true
231
- }]);
232
- }, RESPONSE_TIMEOUT_MS);
233
- return () => clearTimeout(timer);
234
- }, [awaitingResponse]);
235
- return {
236
- items,
237
- setItems,
238
- sending,
239
- awaitingResponse,
240
- roomId,
241
- setRoomId,
242
- send: useCallback(async (message) => {
243
- if (!enabled || sending || !agentUuid) return;
244
- setItems((prev) => [...prev, {
245
- content: message,
246
- byUser: true
247
- }]);
248
- setSending(true);
249
- setConnectionError(false);
250
- try {
251
- const res = await http.post(`integrations/embed/${agentUuid}/execute`, {
252
- message,
253
- guestId,
254
- ...extraPayloadRef.current
255
- });
256
- if (!res.ok) {
257
- const msg = res.data && typeof res.data === "object" && "message" in res.data ? res.data.message : "Something went wrong. Please try again.";
258
- setItems((prev) => [...prev, {
259
- content: msg,
260
- byUser: false,
261
- error: true
262
- }]);
263
- return;
264
- }
265
- setAwaitingResponse(true);
266
- const newRoomId = res.data?.roomId;
267
- if (newRoomId && !roomIdRef.current) {
268
- setRoomId(newRoomId);
269
- saveRoom(roomStorageKey, newRoomId);
270
- }
271
- } catch {
272
- setItems((prev) => [...prev, {
273
- content: "Something went wrong. Please try again.",
274
- byUser: false,
275
- error: true
276
- }]);
277
- } finally {
278
- setSending(false);
279
- }
280
- }, [
281
- enabled,
282
- sending,
283
- agentUuid,
284
- http,
285
- guestId,
286
- roomStorageKey
287
- ]),
288
- connectionError
289
- };
290
- }
291
- //#endregion
292
- //#region src/react/content/context.ts
293
- const ContentContext = createContext(null);
294
- function useContent() {
295
- const context = useContext(ContentContext);
296
- if (!context) throw new Error("useContent must be used within a ContentProvider.");
297
- return context;
298
- }
299
- //#endregion
300
- //#region src/react/content/content-cache.ts
301
- const contentCache = /* @__PURE__ */ new Map();
302
- const pendingLoads = /* @__PURE__ */ new Map();
303
- function cacheContent(content) {
304
- contentCache.set(`id:${content.id}`, content);
305
- contentCache.set(`slug:${content.slug}`, content);
306
- }
307
- async function loadContent(client, contentId, publicSlug) {
308
- const target = contentId ?? publicSlug;
309
- if (!target) return null;
310
- const key = contentId ? `id:${contentId}` : `slug:${publicSlug}`;
311
- const cached = contentCache.get(key);
312
- if (cached) return cached;
313
- const pending = pendingLoads.get(key);
314
- if (pending) return pending;
315
- const request = (contentId ? client.content.get(target) : client.content.getPublic(target)).then((result) => result.ok && result.data ? result.data : null).then((content) => {
316
- if (content) cacheContent(content);
317
- return content;
318
- }).finally(() => pendingLoads.delete(key));
319
- pendingLoads.set(key, request);
320
- return request;
321
- }
322
- //#endregion
323
- //#region src/react/content/state.ts
324
- function contentValues(content) {
325
- return Object.fromEntries(content?.version?.entries.map((entry) => [entry.key, entry.value]) ?? []);
326
- }
327
- function contentPatches(content, values) {
328
- const original = contentValues(content);
329
- return Object.entries(values).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(original[key])).map(([key, value]) => ({
330
- key,
331
- value
332
- }));
333
- }
334
- //#endregion
335
- //#region src/react/content/provider.tsx
336
- function ContentProvider({ children, client, contentId, publicSlug, initialContent, canEdit = false, canPublish = false, teamSlug }) {
337
- const [content, setContent] = useState(initialContent ?? null);
338
- const [values, setValues] = useState(() => contentValues(initialContent ?? null));
339
- const [isLoading, setIsLoading] = useState(!initialContent && Boolean(contentId || publicSlug));
340
- const [isSaving, setIsSaving] = useState(false);
341
- const [isEditing, setEditing] = useState(false);
342
- const [error, setError] = useState(null);
343
- useEffect(() => {
344
- if (initialContent && !contentId) {
345
- cacheContent(initialContent);
346
- setContent(initialContent);
347
- setValues(contentValues(initialContent));
348
- setIsLoading(false);
349
- return;
350
- }
351
- const load = async () => {
352
- if (!contentId && !publicSlug) return;
353
- setIsLoading(true);
354
- setError(null);
355
- const nextContent = await loadContent(client, contentId, publicSlug);
356
- if (!nextContent) setError("Content could not be loaded.");
357
- else {
358
- setContent(nextContent);
359
- setValues(contentValues(nextContent));
360
- }
361
- setIsLoading(false);
362
- };
363
- load();
364
- }, [
365
- client,
366
- contentId,
367
- initialContent,
368
- publicSlug
369
- ]);
370
- const setValue = useCallback((key, value) => {
371
- setValues((current) => ({
372
- ...current,
373
- [key]: value
374
- }));
375
- }, []);
376
- const isDirty = contentPatches(content, values).length > 0;
377
- const save = useCallback(async () => {
378
- if (!content?.version || !contentId || !canEdit) return false;
379
- const patches = contentPatches(content, values);
380
- if (patches.length === 0) return true;
381
- setIsSaving(true);
382
- setError(null);
383
- const result = await client.content.patch(contentId, {
384
- expectedVersion: content.version.number,
385
- patches,
386
- origin: "inlineEditor"
387
- });
388
- setIsSaving(false);
389
- if (!result.ok || !result.data) {
390
- setError("Changes could not be saved. Refresh and try again.");
391
- return false;
392
- }
393
- const nextContent = {
394
- ...content,
395
- version: result.data,
396
- updatedAt: result.data.updatedAt
397
- };
398
- cacheContent(nextContent);
399
- setContent(nextContent);
400
- setValues(contentValues(nextContent));
401
- return true;
402
- }, [
403
- canEdit,
404
- client,
405
- content,
406
- contentId,
407
- values
408
- ]);
409
- const publish = useCallback(async () => {
410
- if (!content?.version || !contentId || !canPublish) return false;
411
- if (!await save()) return false;
412
- const expectedVersion = content.version.number + (contentPatches(content, values).length > 0 ? 1 : 0);
413
- setIsSaving(true);
414
- const result = await client.content.publish(contentId, expectedVersion);
415
- setIsSaving(false);
416
- if (!result.ok || !result.data) {
417
- setError("Changes could not be published. Refresh and try again.");
418
- return false;
419
- }
420
- const nextContent = {
421
- ...content,
422
- version: result.data,
423
- updatedAt: result.data.updatedAt
424
- };
425
- cacheContent(nextContent);
426
- setContent(nextContent);
427
- setValues(contentValues(nextContent));
428
- setEditing(false);
429
- return true;
430
- }, [
431
- canPublish,
432
- client,
433
- content,
434
- contentId,
435
- save,
436
- values
437
- ]);
438
- const context = useMemo(() => ({
439
- content,
440
- values,
441
- isLoading,
442
- isSaving,
443
- isEditing,
444
- isDirty,
445
- canEdit,
446
- canPublish,
447
- teamSlug,
448
- media: client.media,
449
- error,
450
- setEditing,
451
- setValue,
452
- save,
453
- publish
454
- }), [
455
- canEdit,
456
- canPublish,
457
- client.media,
458
- content,
459
- error,
460
- isDirty,
461
- isEditing,
462
- isLoading,
463
- isSaving,
464
- publish,
465
- save,
466
- setValue,
467
- teamSlug,
468
- values
469
- ]);
470
- return /* @__PURE__ */ jsx(ContentContext.Provider, {
471
- value: context,
472
- children
473
- });
474
- }
475
- //#endregion
476
- //#region src/react/content/editable-content.tsx
477
- function EditableContent({ field, as: Tag = "span", className, placeholder = "Add text", multiline = false }) {
478
- const { values, isEditing, canEdit, setValue } = useContent();
479
- const value = values[field];
480
- const text = typeof value === "string" || typeof value === "number" ? String(value) : "";
481
- if (!canEdit || !isEditing) return /* @__PURE__ */ jsx(Tag, {
482
- className,
483
- children: text
484
- });
485
- if (multiline) return /* @__PURE__ */ jsx("textarea", {
486
- "aria-label": field,
487
- className,
488
- placeholder,
489
- value: text,
490
- onChange: (event) => setValue(field, event.target.value)
491
- });
492
- return /* @__PURE__ */ jsx("input", {
493
- "aria-label": field,
494
- className,
495
- placeholder,
496
- value: text,
497
- onChange: (event) => setValue(field, event.target.value)
498
- });
499
- }
500
- function useContentValue(key) {
501
- return useContent().values[key];
502
- }
503
- //#endregion
504
- //#region src/react/content/editable-image.tsx
505
- function EditableImage({ field, alt = "", className, inputClassName, placeholder = "Image URL", onSelect }) {
506
- const { values, isEditing, canEdit, media, setValue, teamSlug } = useContent();
507
- const source = typeof values[field] === "string" ? values[field] : "";
508
- const [isPickerOpen, setPickerOpen] = useState(false);
509
- const [images, setImages] = useState([]);
510
- const [isUploading, setUploading] = useState(false);
511
- useEffect(() => {
512
- if (!isPickerOpen || !media || !teamSlug) return;
513
- media.list(teamSlug, { mime: "image/" }).then((result) => setImages(result.ok ? result.data ?? [] : []));
514
- }, [
515
- isPickerOpen,
516
- media,
517
- teamSlug
518
- ]);
519
- const upload = async (file) => {
520
- if (!media || !teamSlug || !file.type.startsWith("image/")) return;
521
- setUploading(true);
522
- const result = await media.upload({
523
- team: teamSlug,
524
- file,
525
- type: "output",
526
- label: file.name
527
- });
528
- setUploading(false);
529
- if (!result.ok || !result.data) return;
530
- setImages((current) => [result.data, ...current]);
531
- setValue(field, result.data.url);
532
- setPickerOpen(false);
533
- };
534
- if (!canEdit || !isEditing) return source ? /* @__PURE__ */ jsx("img", {
535
- src: source,
536
- alt,
537
- className
538
- }) : null;
539
- return /* @__PURE__ */ jsxs("div", {
540
- className,
541
- children: [
542
- source && /* @__PURE__ */ jsx("img", {
543
- src: source,
544
- alt
545
- }),
546
- onSelect && /* @__PURE__ */ jsx("button", {
547
- className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500",
548
- type: "button",
549
- onClick: onSelect,
550
- children: "Choose image"
551
- }),
552
- !onSelect && media && teamSlug && /* @__PURE__ */ jsx("button", {
553
- className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500",
554
- type: "button",
555
- onClick: () => setPickerOpen(true),
556
- children: "Choose image"
557
- }),
558
- /* @__PURE__ */ jsx(ImagePicker, {
559
- open: isPickerOpen,
560
- onClose: () => setPickerOpen(false),
561
- images,
562
- isUploading,
563
- onUpload: upload,
564
- onSelect: (url) => {
565
- setValue(field, url);
566
- setPickerOpen(false);
567
- }
568
- }),
569
- /* @__PURE__ */ jsx("input", {
570
- "aria-label": field,
571
- className: inputClassName,
572
- type: "url",
573
- placeholder,
574
- value: source,
575
- onChange: (event) => setValue(field, event.target.value)
576
- })
577
- ]
578
- });
579
- }
580
- function ImagePicker({ open, onClose, images, isUploading, onUpload, onSelect }) {
581
- if (!open) return null;
582
- return /* @__PURE__ */ jsx("div", {
583
- className: "fixed inset-0 z-[1100] grid place-items-center bg-slate-950/45 p-4",
584
- role: "dialog",
585
- "aria-modal": "true",
586
- "aria-label": "Image library",
587
- children: /* @__PURE__ */ jsxs("div", {
588
- className: "max-h-[min(44rem,calc(100vh-2rem))] w-full max-w-2xl overflow-y-auto rounded-xl bg-white p-5 shadow-2xl",
589
- children: [
590
- /* @__PURE__ */ jsxs("div", {
591
- className: "flex items-start justify-between gap-4",
592
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
593
- className: "text-lg font-semibold text-slate-950",
594
- children: "Image library"
595
- }), /* @__PURE__ */ jsx("p", {
596
- className: "mt-1 text-sm text-slate-500",
597
- children: "Choose an existing image or upload a new one."
598
- })] }), /* @__PURE__ */ jsx("button", {
599
- className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100",
600
- type: "button",
601
- onClick: onClose,
602
- children: "Close"
603
- })]
604
- }),
605
- /* @__PURE__ */ jsxs("label", {
606
- className: "mt-5 flex cursor-pointer items-center justify-center rounded-lg border border-dashed border-slate-300 px-4 py-6 text-sm font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600",
607
- children: [/* @__PURE__ */ jsx("input", {
608
- className: "sr-only",
609
- type: "file",
610
- accept: "image/*",
611
- onChange: (event) => {
612
- const file = event.target.files?.[0];
613
- if (file) onUpload(file);
614
- }
615
- }), "Upload image"]
616
- }),
617
- isUploading && /* @__PURE__ */ jsx("span", {
618
- className: "mt-3 block text-sm text-slate-500",
619
- children: "Uploading…"
620
- }),
621
- /* @__PURE__ */ jsx("div", {
622
- className: "mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3",
623
- role: "list",
624
- children: images.map((image) => /* @__PURE__ */ jsxs("button", {
625
- className: "overflow-hidden rounded-lg border border-slate-200 text-left transition-opacity hover:opacity-75",
626
- type: "button",
627
- onClick: () => onSelect(image.url),
628
- children: [/* @__PURE__ */ jsx("img", {
629
- className: "aspect-square w-full object-cover",
630
- src: image.url,
631
- alt: image.name
632
- }), /* @__PURE__ */ jsx("span", {
633
- className: "block truncate p-2 text-xs text-slate-700",
634
- children: image.name
635
- })]
636
- }, image.id))
637
- })
638
- ]
639
- })
640
- });
641
- }
642
- //#endregion
643
- //#region src/react/content/toolbar.tsx
644
- function ContentToolbar({ className, labels = {}, confirmPublish }) {
645
- const { canEdit, canPublish, isDirty, isEditing, isSaving, error, setEditing, save, publish } = useContent();
646
- if (!canEdit) return null;
647
- const edit = labels.edit ?? "Edit";
648
- const saveLabel = labels.save ?? "Save draft";
649
- const publishLabel = labels.publish ?? "Publish";
650
- const cancel = labels.cancel ?? "Cancel";
651
- const handlePublish = async () => {
652
- if (confirmPublish && !await confirmPublish()) return;
653
- await publish();
654
- };
655
- return /* @__PURE__ */ jsxs("div", {
656
- className: `flex flex-wrap items-center gap-2 ${className ?? ""}`,
657
- role: "toolbar",
658
- "aria-label": "Content editor",
659
- children: [
660
- !isEditing ? /* @__PURE__ */ jsx("button", {
661
- className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500",
662
- type: "button",
663
- onClick: () => setEditing(true),
664
- children: edit
665
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [
666
- /* @__PURE__ */ jsx("button", {
667
- className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 disabled:opacity-60",
668
- type: "button",
669
- disabled: isSaving,
670
- onClick: () => void save(),
671
- children: isSaving ? "Saving…" : saveLabel
672
- }),
673
- canPublish && /* @__PURE__ */ jsx("button", {
674
- className: "rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-emerald-500 disabled:opacity-60",
675
- type: "button",
676
- disabled: isSaving,
677
- onClick: () => void handlePublish(),
678
- children: publishLabel
679
- }),
680
- /* @__PURE__ */ jsx("button", {
681
- className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 disabled:opacity-60",
682
- type: "button",
683
- disabled: isSaving,
684
- onClick: () => setEditing(false),
685
- children: cancel
686
- })
687
- ] }),
688
- error && /* @__PURE__ */ jsx("span", {
689
- className: "w-full text-sm text-red-600 dark:text-red-400",
690
- role: "alert",
691
- children: error
692
- }),
693
- isEditing && isDirty && /* @__PURE__ */ jsx("span", {
694
- className: "w-full text-xs text-slate-500",
695
- "aria-live": "polite",
696
- children: "Unsaved changes"
697
- })
698
- ]
699
- });
700
- }
701
- //#endregion
702
- //#region src/react/content/content-editor.tsx
703
- function EditorPanel({ title }) {
704
- const { canEdit } = useContent();
705
- if (!canEdit) return null;
706
- return /* @__PURE__ */ jsxs("div", {
707
- className: "fixed right-4 bottom-4 z-[1000] w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-3 text-slate-950 shadow-xl",
708
- children: [/* @__PURE__ */ jsx("strong", {
709
- className: "mb-2 block text-sm font-semibold",
710
- children: title
711
- }), /* @__PURE__ */ jsx(ContentToolbar, { labels: {
712
- edit: "Edit",
713
- save: "Save draft",
714
- publish: "Publish",
715
- cancel: "Cancel"
716
- } })]
717
- });
718
- }
719
- function ContentEditor({ children, title = "JoAi Content", ...provider }) {
720
- return /* @__PURE__ */ jsx(ContentProvider, {
721
- ...provider,
722
- children: /* @__PURE__ */ jsxs("div", {
723
- "data-joai-content-editor": "true",
724
- children: [/* @__PURE__ */ jsx(EditorPanel, { title }), children]
725
- })
726
- });
727
- }
728
- //#endregion
729
- export { ContentEditor, ContentProvider, ContentToolbar, EditableContent, EditableImage, EmbedChatInput, EmbedChatList, clearRoom, getOrCreateGuestId, loadRoom, openEventSourceStream, saveRoom, useContentValue, useEmbedChat };