@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.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 ${getAgentSetupPromptUrl(env)}`,
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 };
@@ -1,4 +1,4 @@
1
- import { n as IHttpService } from "../types-CWSaEVa7.mjs";
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
- export { EmbedChatInput, EmbedChatItem, EmbedChatList, StreamDataHandler, clearRoom, getOrCreateGuestId, loadRoom, openEventSourceStream, saveRoom, useEmbedChat };
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 };
@@ -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
- export { EmbedChatInput, EmbedChatList, clearRoom, getOrCreateGuestId, loadRoom, openEventSourceStream, saveRoom, useEmbedChat };
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 };