@joai/core 4.16.0 → 5.0.1
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.mts +342 -151
- package/dist/index.mjs +238 -2
- package/package.json +2 -13
- package/dist/react/index.d.mts +0 -362
- package/dist/react/index.mjs +0 -729
package/dist/react/index.mjs
DELETED
|
@@ -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
|
-
contentId,
|
|
366
|
-
initialContent,
|
|
367
|
-
publicSlug
|
|
368
|
-
]);
|
|
369
|
-
const setValue = useCallback((key, value) => {
|
|
370
|
-
setValues((current) => ({
|
|
371
|
-
...current,
|
|
372
|
-
[key]: value
|
|
373
|
-
}));
|
|
374
|
-
}, []);
|
|
375
|
-
const isDirty = contentPatches(content, values).length > 0;
|
|
376
|
-
const save = useCallback(async () => {
|
|
377
|
-
if (!content?.version || !contentId || !canEdit) return false;
|
|
378
|
-
const patches = contentPatches(content, values);
|
|
379
|
-
if (patches.length === 0) return true;
|
|
380
|
-
setIsSaving(true);
|
|
381
|
-
setError(null);
|
|
382
|
-
const result = await client.content.patch(contentId, {
|
|
383
|
-
expectedVersion: content.version.number,
|
|
384
|
-
patches,
|
|
385
|
-
origin: "inlineEditor"
|
|
386
|
-
});
|
|
387
|
-
setIsSaving(false);
|
|
388
|
-
if (!result.ok || !result.data) {
|
|
389
|
-
setError("Changes could not be saved. Refresh and try again.");
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
const nextContent = {
|
|
393
|
-
...content,
|
|
394
|
-
version: result.data,
|
|
395
|
-
updatedAt: result.data.updatedAt
|
|
396
|
-
};
|
|
397
|
-
cacheContent(nextContent);
|
|
398
|
-
setContent(nextContent);
|
|
399
|
-
setValues(contentValues(nextContent));
|
|
400
|
-
return true;
|
|
401
|
-
}, [
|
|
402
|
-
canEdit,
|
|
403
|
-
content,
|
|
404
|
-
contentId,
|
|
405
|
-
values
|
|
406
|
-
]);
|
|
407
|
-
const publish = useCallback(async () => {
|
|
408
|
-
if (!content?.version || !contentId || !canPublish) return false;
|
|
409
|
-
const patches = contentPatches(content, values);
|
|
410
|
-
setIsSaving(true);
|
|
411
|
-
setError(null);
|
|
412
|
-
const result = patches.length > 0 ? await client.content.patch(contentId, {
|
|
413
|
-
expectedVersion: content.version.number,
|
|
414
|
-
patches,
|
|
415
|
-
origin: "inlineEditor",
|
|
416
|
-
publish: true
|
|
417
|
-
}) : await client.content.publish(contentId, content.version.number);
|
|
418
|
-
setIsSaving(false);
|
|
419
|
-
if (!result.ok || !result.data) {
|
|
420
|
-
setError("Changes could not be published. Refresh and try again.");
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
const nextContent = {
|
|
424
|
-
...content,
|
|
425
|
-
version: result.data,
|
|
426
|
-
updatedAt: result.data.updatedAt
|
|
427
|
-
};
|
|
428
|
-
cacheContent(nextContent);
|
|
429
|
-
setContent(nextContent);
|
|
430
|
-
setValues(contentValues(nextContent));
|
|
431
|
-
setEditing(false);
|
|
432
|
-
return true;
|
|
433
|
-
}, [
|
|
434
|
-
canPublish,
|
|
435
|
-
content,
|
|
436
|
-
contentId,
|
|
437
|
-
values
|
|
438
|
-
]);
|
|
439
|
-
const context = useMemo(() => ({
|
|
440
|
-
content,
|
|
441
|
-
values,
|
|
442
|
-
isLoading,
|
|
443
|
-
isSaving,
|
|
444
|
-
isEditing,
|
|
445
|
-
isDirty,
|
|
446
|
-
canEdit,
|
|
447
|
-
canPublish,
|
|
448
|
-
teamSlug,
|
|
449
|
-
media: client.media,
|
|
450
|
-
error,
|
|
451
|
-
setEditing,
|
|
452
|
-
setValue,
|
|
453
|
-
save,
|
|
454
|
-
publish
|
|
455
|
-
}), [
|
|
456
|
-
canEdit,
|
|
457
|
-
canPublish,
|
|
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 };
|