@joai/core 4.5.0 → 4.6.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.mts +340 -368
- package/dist/index.mjs +34 -2
- package/dist/react/index.d.mts +144 -2
- package/dist/react/index.mjs +369 -2
- package/dist/types-BbqlPvhU.d.mts +128 -0
- package/package.json +1 -1
- package/dist/types-CWSaEVa7.d.mts +0 -32
package/dist/index.mjs
CHANGED
|
@@ -42,6 +42,7 @@ const getAgentsBaseUrl = (env) => {
|
|
|
42
42
|
if (env === "testnet") return "https://testnet-agents.joai.ai";
|
|
43
43
|
return "https://agents.joai.ai";
|
|
44
44
|
};
|
|
45
|
+
const getMcpServerUrl = (env) => `${getCortexBaseUrl(env)}/mcp`;
|
|
45
46
|
const getMcpServerAgentUrl = (env, agent) => `${getCortexBaseUrl(env)}/mcp/agents/${agent}`;
|
|
46
47
|
const getSitesBaseUrl = (env) => {
|
|
47
48
|
if (env === "local") return "http://sites.joai--api.test:3007";
|
|
@@ -370,10 +371,29 @@ const ContractCategoryDisplayNames = {
|
|
|
370
371
|
};
|
|
371
372
|
//#endregion
|
|
372
373
|
//#region src/mcp/agent-setup.ts
|
|
374
|
+
const UNIFIED_MCP_SERVER_NAME = "joai";
|
|
375
|
+
const AGENT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
376
|
+
const isUnifiedMcpUrl = (mcpUrl) => {
|
|
377
|
+
try {
|
|
378
|
+
return (new URL(mcpUrl).pathname.replace(/\/+$/, "") || "/") === "/mcp";
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
373
383
|
const getAgentSetupPromptUrl = (env) => `${getAppBaseUrl(env)}/prompts/setup-agent.md`;
|
|
374
384
|
const buildAgentSetupClipboardText = (env, target) => {
|
|
385
|
+
const promptUrl = getAgentSetupPromptUrl(env);
|
|
386
|
+
if (isUnifiedMcpUrl(target.mcpUrl)) return [
|
|
387
|
+
`Fetch and execute the appropriate instructions to set me up for JoAi from ${promptUrl}`,
|
|
388
|
+
"",
|
|
389
|
+
"Connect me to JoAi MCP:",
|
|
390
|
+
`- MCP: ${target.mcpUrl}`,
|
|
391
|
+
`- Agent: ${target.agentName}`,
|
|
392
|
+
"",
|
|
393
|
+
"Authenticate with OAuth. The agent in the MCP URL is preselected and can be changed."
|
|
394
|
+
].join("\n");
|
|
375
395
|
return [
|
|
376
|
-
`Fetch and execute the appropriate instructions to set me up for JoAi from ${
|
|
396
|
+
`Fetch and execute the appropriate instructions to set me up for JoAi from ${promptUrl}`,
|
|
377
397
|
"",
|
|
378
398
|
"Connect me to this JoAi agent:",
|
|
379
399
|
`- Agent: ${target.agentName}`,
|
|
@@ -396,6 +416,16 @@ const createCursorMcpInstallLink = (mcpServerUrl, serverName) => {
|
|
|
396
416
|
const encodedConfig = encodeURIComponent(btoa(configJson));
|
|
397
417
|
return `https://cursor.com/en-US/install-mcp?name=${encodeURIComponent(serverName)}&config=${encodedConfig}`;
|
|
398
418
|
};
|
|
419
|
+
/** Default owner/connect MCP URL. Optional agent UUID soft-prefills OAuth via `?agent=`. */
|
|
420
|
+
const resolveConnectMcpUrl = (env, mcpEndpoint, preferredAgentUuid) => {
|
|
421
|
+
const trimmed = mcpEndpoint?.trim();
|
|
422
|
+
const base = trimmed && isUnifiedMcpUrl(trimmed) ? `${new URL(trimmed).origin}${new URL(trimmed).pathname.replace(/\/+$/, "") || "/mcp"}` : `${getCortexBaseUrl(env)}/mcp`;
|
|
423
|
+
const fromUrl = trimmed && isUnifiedMcpUrl(trimmed) ? new URL(trimmed).searchParams.get("agent") || new URL(trimmed).searchParams.get("agent_uuid") : null;
|
|
424
|
+
const preferred = preferredAgentUuid?.trim() || fromUrl;
|
|
425
|
+
if (preferred && AGENT_UUID_RE.test(preferred)) return `${base}?agent=${encodeURIComponent(preferred)}`;
|
|
426
|
+
return base;
|
|
427
|
+
};
|
|
428
|
+
/** Agent-scoped MCP URL for public/direct addressing (Agent0, x402, marketplace). */
|
|
399
429
|
const resolveAgentMcpUrl = (env, agentUuid, mcpEndpoint) => mcpEndpoint?.trim() || `${getCortexBaseUrl(env)}/mcp/agents/${agentUuid}`;
|
|
400
430
|
const slugifyMcpSegment = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
401
431
|
const buildMcpServerName = (slug, teamName) => {
|
|
@@ -500,6 +530,8 @@ const UserPermissions = {
|
|
|
500
530
|
BillingManage: "billing-manage",
|
|
501
531
|
TeamManage: "team-manage",
|
|
502
532
|
TeamView: "team-view",
|
|
533
|
+
ContentEdit: "content-edit",
|
|
534
|
+
ContentPublish: "content-publish",
|
|
503
535
|
TeamMemberInvite: "team-member-invite",
|
|
504
536
|
TeamMemberRemove: "team-member-remove",
|
|
505
537
|
TeamSecretView: "team-secret-view",
|
|
@@ -585,4 +617,4 @@ const DefaultInviteTeamRole = UserRoles.Member;
|
|
|
585
617
|
const inviteableTeamRoles = () => [...InviteableTeamRoles];
|
|
586
618
|
const defaultInviteTeamRole = () => DefaultInviteTeamRole;
|
|
587
619
|
//#endregion
|
|
588
|
-
export { AddressTypes, AgentToolingAccess, BillingManageAccess, ContractCategoryDisplayNames, DefaultCodeMetadata, DefaultInviteTeamRole, FulfillmentMethods, FulfillmentStatuses, HttpService, InviteableTeamRoles, JoaiUrls, NATIVE_AUTH_TOKEN_PARAM, NativeApps, OG_IMAGE_CONFIG, OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH, OgImageColors, PeriodTypeLabels, PeriodTypeOptions, PermissionPresetNames, ReturnStatuses, ReturnUpdateStatuses, SkillMediaBindingRole, SkillScope, SkillType, TeamManageAccess, TeamViewAccess, UserPermissions, UserRoles, abbreviateNumber, addressTypeValues, buildAgentMcpServerName, buildAgentSetupClipboardText, buildAppSetupClipboardText, buildMcpServerName, cartLineTotal, createCursorMcpInstallLink, defaultInviteTeamRole, fulfillmentMethodValues, fulfillmentStatusValues, getAgentSetupPromptUrl, getAgentsBaseUrl, getApiBaseUrl, getAppBaseUrl, getAssetAppLogoUrl, getAssetChainLogoUrl, getAssetPartnerLogoUrl, getAssetTokenLogoUrl, getAssetWalletLogoUrl, getCortexBaseUrl, getDigestBaseUrl, getFormsBaseUrl, getLoginUrl, getMcpServerAgentUrl, getSiteUrl, getSitesBaseUrl, getSitesRootDomain, getStoreBaseUrl, getWarpScreenshotUrl, hasAllPermissions, hasAllTeamPermissions, hasAnyPermissions, hasPermission, hasTeamPermission, inviteableTeamRoles, isCompleteAddressInput, isFileFormatAudio, isFileFormatImage, isFileFormatVideo, isNativeApp, isNativeAppAccessible, isNativeAppGated, isNativeAppLocked, isTeamFeatureAccessible, isTeamFeatureGated, isTeamFeatureLocked, isUserRole, localize, nativeAppSlugs, ogImageUrl, permissionPresetRules, resolveAccessCatalog, resolveAgentMcpUrl, resolveWarpText, returnStatusValues, returnUpdateStatusValues, sanitizeAlphanumeric, sanitizeNumeric, slugifyMcpSegment, teamCategories, toCurrencyDisplay, toHexEncodedNonce, toNameInitials, toNftId, toPreviewText, userPermissionValues, userRoleValues, withRetry };
|
|
620
|
+
export { AddressTypes, AgentToolingAccess, BillingManageAccess, ContractCategoryDisplayNames, DefaultCodeMetadata, DefaultInviteTeamRole, FulfillmentMethods, FulfillmentStatuses, HttpService, InviteableTeamRoles, JoaiUrls, NATIVE_AUTH_TOKEN_PARAM, NativeApps, OG_IMAGE_CONFIG, OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH, OgImageColors, PeriodTypeLabels, PeriodTypeOptions, PermissionPresetNames, ReturnStatuses, ReturnUpdateStatuses, SkillMediaBindingRole, SkillScope, SkillType, TeamManageAccess, TeamViewAccess, UNIFIED_MCP_SERVER_NAME, UserPermissions, UserRoles, abbreviateNumber, addressTypeValues, buildAgentMcpServerName, buildAgentSetupClipboardText, buildAppSetupClipboardText, buildMcpServerName, cartLineTotal, createCursorMcpInstallLink, defaultInviteTeamRole, fulfillmentMethodValues, fulfillmentStatusValues, getAgentSetupPromptUrl, getAgentsBaseUrl, getApiBaseUrl, getAppBaseUrl, getAssetAppLogoUrl, getAssetChainLogoUrl, getAssetPartnerLogoUrl, getAssetTokenLogoUrl, getAssetWalletLogoUrl, getCortexBaseUrl, getDigestBaseUrl, getFormsBaseUrl, getLoginUrl, getMcpServerAgentUrl, getMcpServerUrl, getSiteUrl, getSitesBaseUrl, getSitesRootDomain, getStoreBaseUrl, getWarpScreenshotUrl, hasAllPermissions, hasAllTeamPermissions, hasAnyPermissions, hasPermission, hasTeamPermission, inviteableTeamRoles, isCompleteAddressInput, isFileFormatAudio, isFileFormatImage, isFileFormatVideo, isNativeApp, isNativeAppAccessible, isNativeAppGated, isNativeAppLocked, isTeamFeatureAccessible, isTeamFeatureGated, isTeamFeatureLocked, isUnifiedMcpUrl, isUserRole, localize, nativeAppSlugs, ogImageUrl, permissionPresetRules, resolveAccessCatalog, resolveAgentMcpUrl, resolveConnectMcpUrl, resolveWarpText, returnStatusValues, returnUpdateStatusValues, sanitizeAlphanumeric, sanitizeNumeric, slugifyMcpSegment, teamCategories, toCurrencyDisplay, toHexEncodedNonce, toNameInitials, toNftId, toPreviewText, userPermissionValues, userRoleValues, withRetry };
|
package/dist/react/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { b as IHttpService, c as ContentValue, d as PatchContentInput, l as ContentVersion, m as Media, t as Content } from "../types-BbqlPvhU.mjs";
|
|
2
2
|
import * as _$react from "react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
@@ -101,4 +101,146 @@ declare function useEmbedChat({
|
|
|
101
101
|
connectionError: boolean;
|
|
102
102
|
};
|
|
103
103
|
//#endregion
|
|
104
|
-
|
|
104
|
+
//#region src/react/content/types.d.ts
|
|
105
|
+
type ContentResource = {
|
|
106
|
+
get: (contentId: string) => Promise<{
|
|
107
|
+
ok: boolean;
|
|
108
|
+
data?: Content;
|
|
109
|
+
}>;
|
|
110
|
+
getPublic: (slug: string) => Promise<{
|
|
111
|
+
ok: boolean;
|
|
112
|
+
data?: Content;
|
|
113
|
+
}>;
|
|
114
|
+
patch: (contentId: string, input: PatchContentInput) => Promise<{
|
|
115
|
+
ok: boolean;
|
|
116
|
+
data?: ContentVersion;
|
|
117
|
+
}>;
|
|
118
|
+
publish: (contentId: string, expectedVersion: number) => Promise<{
|
|
119
|
+
ok: boolean;
|
|
120
|
+
data?: ContentVersion;
|
|
121
|
+
}>;
|
|
122
|
+
};
|
|
123
|
+
type ContentClient = {
|
|
124
|
+
content: ContentResource;
|
|
125
|
+
media?: {
|
|
126
|
+
list: (team: string, params?: {
|
|
127
|
+
page?: number;
|
|
128
|
+
mime?: string;
|
|
129
|
+
}) => Promise<{
|
|
130
|
+
ok: boolean;
|
|
131
|
+
data?: Media[];
|
|
132
|
+
}>;
|
|
133
|
+
upload: (data: {
|
|
134
|
+
team: string;
|
|
135
|
+
file: File;
|
|
136
|
+
type: 'output';
|
|
137
|
+
label?: string;
|
|
138
|
+
}) => Promise<{
|
|
139
|
+
ok: boolean;
|
|
140
|
+
data?: Media;
|
|
141
|
+
}>;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
type ContentContextValue = {
|
|
145
|
+
content: Content | null;
|
|
146
|
+
values: Record<string, ContentValue>;
|
|
147
|
+
isLoading: boolean;
|
|
148
|
+
isSaving: boolean;
|
|
149
|
+
isEditing: boolean;
|
|
150
|
+
isDirty: boolean;
|
|
151
|
+
canEdit: boolean;
|
|
152
|
+
canPublish: boolean;
|
|
153
|
+
teamSlug?: string;
|
|
154
|
+
media?: ContentClient['media'];
|
|
155
|
+
error: string | null;
|
|
156
|
+
setEditing: (editing: boolean) => void;
|
|
157
|
+
setValue: (key: string, value: ContentValue) => void;
|
|
158
|
+
save: () => Promise<boolean>;
|
|
159
|
+
publish: () => Promise<boolean>;
|
|
160
|
+
};
|
|
161
|
+
type ContentProviderProp = {
|
|
162
|
+
children: ReactNode;
|
|
163
|
+
client: ContentClient;
|
|
164
|
+
contentId?: string;
|
|
165
|
+
publicSlug?: string;
|
|
166
|
+
initialContent?: Content;
|
|
167
|
+
canEdit?: boolean;
|
|
168
|
+
canPublish?: boolean;
|
|
169
|
+
teamSlug?: string;
|
|
170
|
+
};
|
|
171
|
+
type EditableContentProp = {
|
|
172
|
+
field: string;
|
|
173
|
+
as?: 'span' | 'p' | 'div' | 'h1' | 'h2' | 'h3';
|
|
174
|
+
className?: string;
|
|
175
|
+
placeholder?: string;
|
|
176
|
+
multiline?: boolean;
|
|
177
|
+
};
|
|
178
|
+
type EditableImageProp = {
|
|
179
|
+
field: string;
|
|
180
|
+
alt?: string;
|
|
181
|
+
className?: string;
|
|
182
|
+
onSelect?: () => void;
|
|
183
|
+
inputClassName?: string;
|
|
184
|
+
placeholder?: string;
|
|
185
|
+
};
|
|
186
|
+
type ContentToolbarProp = {
|
|
187
|
+
className?: string;
|
|
188
|
+
confirmPublish?: () => boolean | Promise<boolean>;
|
|
189
|
+
labels?: {
|
|
190
|
+
edit?: string;
|
|
191
|
+
save?: string;
|
|
192
|
+
publish?: string;
|
|
193
|
+
cancel?: string;
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/react/content/provider.d.ts
|
|
198
|
+
declare function ContentProvider({
|
|
199
|
+
children,
|
|
200
|
+
client,
|
|
201
|
+
contentId,
|
|
202
|
+
publicSlug,
|
|
203
|
+
initialContent,
|
|
204
|
+
canEdit,
|
|
205
|
+
canPublish,
|
|
206
|
+
teamSlug
|
|
207
|
+
}: ContentProviderProp): _$react.JSX.Element;
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/react/content/editable-content.d.ts
|
|
210
|
+
declare function EditableContent({
|
|
211
|
+
field,
|
|
212
|
+
as: Tag,
|
|
213
|
+
className,
|
|
214
|
+
placeholder,
|
|
215
|
+
multiline
|
|
216
|
+
}: EditableContentProp): _$react.JSX.Element;
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/react/content/editable-image.d.ts
|
|
219
|
+
declare function EditableImage({
|
|
220
|
+
field,
|
|
221
|
+
alt,
|
|
222
|
+
className,
|
|
223
|
+
inputClassName,
|
|
224
|
+
placeholder,
|
|
225
|
+
onSelect
|
|
226
|
+
}: EditableImageProp): _$react.JSX.Element | null;
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/react/content/toolbar.d.ts
|
|
229
|
+
declare function ContentToolbar({
|
|
230
|
+
className,
|
|
231
|
+
labels,
|
|
232
|
+
confirmPublish
|
|
233
|
+
}: ContentToolbarProp): _$react.JSX.Element | null;
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/react/content/content-editor.d.ts
|
|
236
|
+
type Prop = ContentProviderProp & {
|
|
237
|
+
children: ReactNode;
|
|
238
|
+
title?: string;
|
|
239
|
+
};
|
|
240
|
+
declare function ContentEditor({
|
|
241
|
+
children,
|
|
242
|
+
title,
|
|
243
|
+
...provider
|
|
244
|
+
}: Prop): _$react.JSX.Element;
|
|
245
|
+
//#endregion
|
|
246
|
+
export { ContentClient, ContentContextValue, ContentEditor, ContentProvider, ContentProviderProp, ContentResource, ContentToolbar, ContentToolbarProp, EditableContent, EditableContentProp, EditableImage, EditableImageProp, EmbedChatInput, EmbedChatItem, EmbedChatList, StreamDataHandler, clearRoom, getOrCreateGuestId, loadRoom, openEventSourceStream, saveRoom, useEmbedChat };
|
package/dist/react/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
//#region src/react/guest.ts
|
|
4
4
|
function safeGet(key) {
|
|
@@ -288,4 +288,371 @@ function useEmbedChat({ agentUuid, http, cortexBaseUrl, guestId, roomStorageKey,
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
//#endregion
|
|
291
|
-
|
|
291
|
+
//#region src/react/content/context.ts
|
|
292
|
+
const ContentContext = createContext(null);
|
|
293
|
+
function useContent() {
|
|
294
|
+
const context = useContext(ContentContext);
|
|
295
|
+
if (!context) throw new Error("useContent must be used within a ContentProvider.");
|
|
296
|
+
return context;
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/react/content/state.ts
|
|
300
|
+
function contentValues(content) {
|
|
301
|
+
return Object.fromEntries(content?.version?.entries.map((entry) => [entry.key, entry.value]) ?? []);
|
|
302
|
+
}
|
|
303
|
+
function contentPatches(content, values) {
|
|
304
|
+
const original = contentValues(content);
|
|
305
|
+
return Object.entries(values).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(original[key])).map(([key, value]) => ({
|
|
306
|
+
key,
|
|
307
|
+
value
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/react/content/provider.tsx
|
|
312
|
+
function ContentProvider({ children, client, contentId, publicSlug, initialContent, canEdit = false, canPublish = false, teamSlug }) {
|
|
313
|
+
const [content, setContent] = useState(initialContent ?? null);
|
|
314
|
+
const [values, setValues] = useState(() => contentValues(initialContent ?? null));
|
|
315
|
+
const [isLoading, setIsLoading] = useState(!initialContent && Boolean(contentId || publicSlug));
|
|
316
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
317
|
+
const [isEditing, setEditing] = useState(false);
|
|
318
|
+
const [error, setError] = useState(null);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (initialContent) {
|
|
321
|
+
setContent(initialContent);
|
|
322
|
+
setValues(contentValues(initialContent));
|
|
323
|
+
setIsLoading(false);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const load = async () => {
|
|
327
|
+
if (!contentId && !publicSlug) return;
|
|
328
|
+
setIsLoading(true);
|
|
329
|
+
setError(null);
|
|
330
|
+
const result = contentId ? await client.content.get(contentId) : await client.content.getPublic(publicSlug);
|
|
331
|
+
if (!result.ok || !result.data) setError("Content could not be loaded.");
|
|
332
|
+
else {
|
|
333
|
+
setContent(result.data);
|
|
334
|
+
setValues(contentValues(result.data));
|
|
335
|
+
}
|
|
336
|
+
setIsLoading(false);
|
|
337
|
+
};
|
|
338
|
+
load();
|
|
339
|
+
}, [
|
|
340
|
+
client,
|
|
341
|
+
contentId,
|
|
342
|
+
initialContent,
|
|
343
|
+
publicSlug
|
|
344
|
+
]);
|
|
345
|
+
const setValue = useCallback((key, value) => {
|
|
346
|
+
setValues((current) => ({
|
|
347
|
+
...current,
|
|
348
|
+
[key]: value
|
|
349
|
+
}));
|
|
350
|
+
}, []);
|
|
351
|
+
const isDirty = contentPatches(content, values).length > 0;
|
|
352
|
+
const save = useCallback(async () => {
|
|
353
|
+
if (!content?.version || !contentId || !canEdit) return false;
|
|
354
|
+
const patches = contentPatches(content, values);
|
|
355
|
+
if (patches.length === 0) return true;
|
|
356
|
+
setIsSaving(true);
|
|
357
|
+
setError(null);
|
|
358
|
+
const result = await client.content.patch(contentId, {
|
|
359
|
+
expectedVersion: content.version.number,
|
|
360
|
+
patches,
|
|
361
|
+
origin: "inlineEditor"
|
|
362
|
+
});
|
|
363
|
+
setIsSaving(false);
|
|
364
|
+
if (!result.ok || !result.data) {
|
|
365
|
+
setError("Changes could not be saved. Refresh and try again.");
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const nextContent = {
|
|
369
|
+
...content,
|
|
370
|
+
version: result.data,
|
|
371
|
+
updatedAt: result.data.updatedAt
|
|
372
|
+
};
|
|
373
|
+
setContent(nextContent);
|
|
374
|
+
setValues(contentValues(nextContent));
|
|
375
|
+
return true;
|
|
376
|
+
}, [
|
|
377
|
+
canEdit,
|
|
378
|
+
client,
|
|
379
|
+
content,
|
|
380
|
+
contentId,
|
|
381
|
+
values
|
|
382
|
+
]);
|
|
383
|
+
const publish = useCallback(async () => {
|
|
384
|
+
if (!content?.version || !contentId || !canPublish) return false;
|
|
385
|
+
if (!await save()) return false;
|
|
386
|
+
const expectedVersion = content.version.number + (contentPatches(content, values).length > 0 ? 1 : 0);
|
|
387
|
+
setIsSaving(true);
|
|
388
|
+
const result = await client.content.publish(contentId, expectedVersion);
|
|
389
|
+
setIsSaving(false);
|
|
390
|
+
if (!result.ok || !result.data) {
|
|
391
|
+
setError("Changes could not be published. Refresh and try again.");
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const nextContent = {
|
|
395
|
+
...content,
|
|
396
|
+
version: result.data,
|
|
397
|
+
updatedAt: result.data.updatedAt
|
|
398
|
+
};
|
|
399
|
+
setContent(nextContent);
|
|
400
|
+
setValues(contentValues(nextContent));
|
|
401
|
+
setEditing(false);
|
|
402
|
+
return true;
|
|
403
|
+
}, [
|
|
404
|
+
canPublish,
|
|
405
|
+
client,
|
|
406
|
+
content,
|
|
407
|
+
contentId,
|
|
408
|
+
save,
|
|
409
|
+
values
|
|
410
|
+
]);
|
|
411
|
+
const context = useMemo(() => ({
|
|
412
|
+
content,
|
|
413
|
+
values,
|
|
414
|
+
isLoading,
|
|
415
|
+
isSaving,
|
|
416
|
+
isEditing,
|
|
417
|
+
isDirty,
|
|
418
|
+
canEdit,
|
|
419
|
+
canPublish,
|
|
420
|
+
teamSlug,
|
|
421
|
+
media: client.media,
|
|
422
|
+
error,
|
|
423
|
+
setEditing,
|
|
424
|
+
setValue,
|
|
425
|
+
save,
|
|
426
|
+
publish
|
|
427
|
+
}), [
|
|
428
|
+
canEdit,
|
|
429
|
+
canPublish,
|
|
430
|
+
client.media,
|
|
431
|
+
content,
|
|
432
|
+
error,
|
|
433
|
+
isDirty,
|
|
434
|
+
isEditing,
|
|
435
|
+
isLoading,
|
|
436
|
+
isSaving,
|
|
437
|
+
publish,
|
|
438
|
+
save,
|
|
439
|
+
setValue,
|
|
440
|
+
teamSlug,
|
|
441
|
+
values
|
|
442
|
+
]);
|
|
443
|
+
return /* @__PURE__ */ jsx(ContentContext.Provider, {
|
|
444
|
+
value: context,
|
|
445
|
+
children
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/react/content/editable-content.tsx
|
|
450
|
+
function EditableContent({ field, as: Tag = "span", className, placeholder = "Add text", multiline = false }) {
|
|
451
|
+
const { values, isEditing, canEdit, setValue } = useContent();
|
|
452
|
+
const value = values[field];
|
|
453
|
+
const text = typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
454
|
+
if (!canEdit || !isEditing) return /* @__PURE__ */ jsx(Tag, {
|
|
455
|
+
className,
|
|
456
|
+
children: text
|
|
457
|
+
});
|
|
458
|
+
if (multiline) return /* @__PURE__ */ jsx("textarea", {
|
|
459
|
+
"aria-label": field,
|
|
460
|
+
className,
|
|
461
|
+
placeholder,
|
|
462
|
+
value: text,
|
|
463
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
464
|
+
});
|
|
465
|
+
return /* @__PURE__ */ jsx("input", {
|
|
466
|
+
"aria-label": field,
|
|
467
|
+
className,
|
|
468
|
+
placeholder,
|
|
469
|
+
value: text,
|
|
470
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/react/content/editable-image.tsx
|
|
475
|
+
function EditableImage({ field, alt = "", className, inputClassName, placeholder = "Image URL", onSelect }) {
|
|
476
|
+
const { values, isEditing, canEdit, media, setValue, teamSlug } = useContent();
|
|
477
|
+
const source = typeof values[field] === "string" ? values[field] : "";
|
|
478
|
+
const [isPickerOpen, setPickerOpen] = useState(false);
|
|
479
|
+
const [images, setImages] = useState([]);
|
|
480
|
+
const [isUploading, setUploading] = useState(false);
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (!isPickerOpen || !media || !teamSlug) return;
|
|
483
|
+
media.list(teamSlug, { mime: "image/" }).then((result) => setImages(result.ok ? result.data ?? [] : []));
|
|
484
|
+
}, [
|
|
485
|
+
isPickerOpen,
|
|
486
|
+
media,
|
|
487
|
+
teamSlug
|
|
488
|
+
]);
|
|
489
|
+
const upload = async (file) => {
|
|
490
|
+
if (!media || !teamSlug || !file.type.startsWith("image/")) return;
|
|
491
|
+
setUploading(true);
|
|
492
|
+
const result = await media.upload({
|
|
493
|
+
team: teamSlug,
|
|
494
|
+
file,
|
|
495
|
+
type: "output",
|
|
496
|
+
label: file.name
|
|
497
|
+
});
|
|
498
|
+
setUploading(false);
|
|
499
|
+
if (!result.ok || !result.data) return;
|
|
500
|
+
setImages((current) => [result.data, ...current]);
|
|
501
|
+
setValue(field, result.data.url);
|
|
502
|
+
setPickerOpen(false);
|
|
503
|
+
};
|
|
504
|
+
if (!canEdit || !isEditing) return source ? /* @__PURE__ */ jsx("img", {
|
|
505
|
+
src: source,
|
|
506
|
+
alt,
|
|
507
|
+
className
|
|
508
|
+
}) : null;
|
|
509
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
510
|
+
className,
|
|
511
|
+
children: [
|
|
512
|
+
source && /* @__PURE__ */ jsx("img", {
|
|
513
|
+
src: source,
|
|
514
|
+
alt
|
|
515
|
+
}),
|
|
516
|
+
onSelect && /* @__PURE__ */ jsx("button", {
|
|
517
|
+
type: "button",
|
|
518
|
+
onClick: onSelect,
|
|
519
|
+
children: "Choose image"
|
|
520
|
+
}),
|
|
521
|
+
!onSelect && media && teamSlug && /* @__PURE__ */ jsx("button", {
|
|
522
|
+
type: "button",
|
|
523
|
+
onClick: () => setPickerOpen((open) => !open),
|
|
524
|
+
children: "Choose image"
|
|
525
|
+
}),
|
|
526
|
+
isPickerOpen && /* @__PURE__ */ jsxs("div", {
|
|
527
|
+
role: "dialog",
|
|
528
|
+
"aria-label": "Image picker",
|
|
529
|
+
children: [
|
|
530
|
+
/* @__PURE__ */ jsxs("label", { children: ["Bild hochladen", /* @__PURE__ */ jsx("input", {
|
|
531
|
+
type: "file",
|
|
532
|
+
accept: "image/*",
|
|
533
|
+
disabled: isUploading,
|
|
534
|
+
onChange: (event) => {
|
|
535
|
+
const file = event.target.files?.[0];
|
|
536
|
+
if (file) upload(file);
|
|
537
|
+
}
|
|
538
|
+
})] }),
|
|
539
|
+
isUploading && /* @__PURE__ */ jsx("span", { children: "Uploading…" }),
|
|
540
|
+
/* @__PURE__ */ jsx("div", {
|
|
541
|
+
role: "list",
|
|
542
|
+
children: images.map((image) => /* @__PURE__ */ jsx("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
onClick: () => {
|
|
545
|
+
setValue(field, image.url);
|
|
546
|
+
setPickerOpen(false);
|
|
547
|
+
},
|
|
548
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
549
|
+
src: image.url,
|
|
550
|
+
alt: image.name
|
|
551
|
+
})
|
|
552
|
+
}, image.id))
|
|
553
|
+
})
|
|
554
|
+
]
|
|
555
|
+
}),
|
|
556
|
+
/* @__PURE__ */ jsx("input", {
|
|
557
|
+
"aria-label": field,
|
|
558
|
+
className: inputClassName,
|
|
559
|
+
type: "url",
|
|
560
|
+
placeholder,
|
|
561
|
+
value: source,
|
|
562
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
563
|
+
})
|
|
564
|
+
]
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/react/content/toolbar.tsx
|
|
569
|
+
function ContentToolbar({ className, labels = {}, confirmPublish }) {
|
|
570
|
+
const { canEdit, canPublish, isDirty, isEditing, isSaving, error, setEditing, save, publish } = useContent();
|
|
571
|
+
if (!canEdit) return null;
|
|
572
|
+
const edit = labels.edit ?? "Edit";
|
|
573
|
+
const saveLabel = labels.save ?? "Save draft";
|
|
574
|
+
const publishLabel = labels.publish ?? "Publish";
|
|
575
|
+
const cancel = labels.cancel ?? "Cancel";
|
|
576
|
+
const handlePublish = async () => {
|
|
577
|
+
if (confirmPublish && !await confirmPublish()) return;
|
|
578
|
+
await publish();
|
|
579
|
+
};
|
|
580
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
581
|
+
className,
|
|
582
|
+
role: "toolbar",
|
|
583
|
+
"aria-label": "Content editor",
|
|
584
|
+
children: [
|
|
585
|
+
!isEditing ? /* @__PURE__ */ jsx("button", {
|
|
586
|
+
type: "button",
|
|
587
|
+
onClick: () => setEditing(true),
|
|
588
|
+
children: edit
|
|
589
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
590
|
+
/* @__PURE__ */ jsx("button", {
|
|
591
|
+
type: "button",
|
|
592
|
+
disabled: isSaving,
|
|
593
|
+
onClick: () => void save(),
|
|
594
|
+
children: saveLabel
|
|
595
|
+
}),
|
|
596
|
+
canPublish && /* @__PURE__ */ jsx("button", {
|
|
597
|
+
type: "button",
|
|
598
|
+
disabled: isSaving,
|
|
599
|
+
onClick: () => void handlePublish(),
|
|
600
|
+
children: publishLabel
|
|
601
|
+
}),
|
|
602
|
+
/* @__PURE__ */ jsx("button", {
|
|
603
|
+
type: "button",
|
|
604
|
+
disabled: isSaving,
|
|
605
|
+
onClick: () => setEditing(false),
|
|
606
|
+
children: cancel
|
|
607
|
+
})
|
|
608
|
+
] }),
|
|
609
|
+
error && /* @__PURE__ */ jsx("span", {
|
|
610
|
+
role: "alert",
|
|
611
|
+
children: error
|
|
612
|
+
}),
|
|
613
|
+
isEditing && isDirty && /* @__PURE__ */ jsx("span", {
|
|
614
|
+
"aria-live": "polite",
|
|
615
|
+
children: "Unsaved changes"
|
|
616
|
+
})
|
|
617
|
+
]
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region src/react/content/content-editor.tsx
|
|
622
|
+
function ContentEditor({ children, title = "JoAi Content", ...provider }) {
|
|
623
|
+
return /* @__PURE__ */ jsx(ContentProvider, {
|
|
624
|
+
...provider,
|
|
625
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
626
|
+
"data-joai-content-editor": "true",
|
|
627
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
628
|
+
style: {
|
|
629
|
+
position: "fixed",
|
|
630
|
+
right: 16,
|
|
631
|
+
bottom: 16,
|
|
632
|
+
zIndex: 1e3,
|
|
633
|
+
maxWidth: "calc(100vw - 32px)",
|
|
634
|
+
padding: 12,
|
|
635
|
+
borderRadius: 14,
|
|
636
|
+
background: "#111827",
|
|
637
|
+
color: "#fff",
|
|
638
|
+
boxShadow: "0 14px 40px rgba(0,0,0,.28)"
|
|
639
|
+
},
|
|
640
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
641
|
+
style: {
|
|
642
|
+
display: "block",
|
|
643
|
+
marginBottom: 8,
|
|
644
|
+
fontSize: 13
|
|
645
|
+
},
|
|
646
|
+
children: title
|
|
647
|
+
}), /* @__PURE__ */ jsx(ContentToolbar, { labels: {
|
|
648
|
+
edit: "Edit",
|
|
649
|
+
save: "Save draft",
|
|
650
|
+
publish: "Publish",
|
|
651
|
+
cancel: "Cancel"
|
|
652
|
+
} })]
|
|
653
|
+
}), children]
|
|
654
|
+
})
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
export { ContentEditor, ContentProvider, ContentToolbar, EditableContent, EditableImage, EmbedChatInput, EmbedChatList, clearRoom, getOrCreateGuestId, loadRoom, openEventSourceStream, saveRoom, useEmbedChat };
|