@stigmer/react 3.1.13 → 3.1.14
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/agent/AgentDetailView.d.ts +7 -9
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +22 -30
- package/agent/AgentDetailView.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/sharing/AgentShareList.d.ts +44 -0
- package/sharing/AgentShareList.d.ts.map +1 -0
- package/sharing/AgentShareList.js +149 -0
- package/sharing/AgentShareList.js.map +1 -0
- package/sharing/ShareAgentDialog.d.ts +38 -25
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +131 -62
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sharing/index.d.ts +10 -8
- package/sharing/index.d.ts.map +1 -1
- package/sharing/index.js +5 -4
- package/sharing/index.js.map +1 -1
- package/sharing/useAgentShares.d.ts +42 -0
- package/sharing/useAgentShares.d.ts.map +1 -0
- package/sharing/useAgentShares.js +38 -0
- package/sharing/useAgentShares.js.map +1 -0
- package/sharing/useCanCreateAgentShare.d.ts +43 -0
- package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
- package/sharing/useCanCreateAgentShare.js +44 -0
- package/sharing/useCanCreateAgentShare.js.map +1 -0
- package/sharing/useDeleteAgentShare.d.ts +42 -0
- package/sharing/useDeleteAgentShare.d.ts.map +1 -0
- package/sharing/useDeleteAgentShare.js +45 -0
- package/sharing/useDeleteAgentShare.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +43 -18
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +55 -22
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +34 -43
- package/src/index.ts +11 -9
- package/src/sharing/AgentShareList.tsx +507 -0
- package/src/sharing/ShareAgentDialog.tsx +310 -112
- package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
- package/src/sharing/__tests__/useAgentShares.test.tsx +145 -0
- package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
- package/src/sharing/index.ts +10 -12
- package/src/sharing/useAgentShares.ts +68 -0
- package/src/sharing/useCanCreateAgentShare.ts +74 -0
- package/src/sharing/useDeleteAgentShare.ts +73 -0
- package/src/sharing/useSaveAgentShare.ts +73 -23
- package/styles.css +1 -1
- package/sharing/useAgentShare.d.ts +0 -49
- package/sharing/useAgentShare.d.ts.map +0 -1
- package/sharing/useAgentShare.js +0 -64
- package/sharing/useAgentShare.js.map +0 -1
- package/sharing/useCreateExternalShareLink.d.ts +0 -90
- package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
- package/sharing/useCreateExternalShareLink.js +0 -65
- package/sharing/useCreateExternalShareLink.js.map +0 -1
- package/sharing/useShareAgent.d.ts +0 -69
- package/sharing/useShareAgent.d.ts.map +0 -1
- package/sharing/useShareAgent.js +0 -42
- package/sharing/useShareAgent.js.map +0 -1
- package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
- package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
- package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
- package/src/sharing/useAgentShare.ts +0 -105
- package/src/sharing/useCreateExternalShareLink.tsx +0 -141
- package/src/sharing/useShareAgent.tsx +0 -107
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useMemo, useRef, useState, type FormEvent } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import {
|
|
6
6
|
MAX_ALLOWED_ORIGINS,
|
|
7
|
+
StigmerError,
|
|
7
8
|
appendLinkToken,
|
|
8
9
|
buildEmbedSnippet,
|
|
9
10
|
chatPath,
|
|
@@ -11,10 +12,12 @@ import {
|
|
|
11
12
|
validateOrigin,
|
|
12
13
|
type ResourceRef,
|
|
13
14
|
} from "@stigmer/sdk";
|
|
15
|
+
import { create as createMessage } from "@bufbuild/protobuf";
|
|
14
16
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
15
17
|
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
16
18
|
import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
17
19
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
20
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
18
21
|
import { Switch } from "../switch/Switch.js";
|
|
19
22
|
import { Tabs, type TabItem } from "../tabs/Tabs.js";
|
|
20
23
|
import { toast } from "../feedback/toast.js";
|
|
@@ -23,9 +26,10 @@ import { useDeploymentMode } from "../deployment-mode.js";
|
|
|
23
26
|
import { useBillingAccount } from "../billing/useBillingAccount.js";
|
|
24
27
|
import { formatCreditBalance } from "../billing/format.js";
|
|
25
28
|
import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
|
|
26
|
-
import {
|
|
29
|
+
import { generateSlug } from "../internal/slug.js";
|
|
30
|
+
import { getFieldError, validateMessage } from "../internal/validate.js";
|
|
27
31
|
import {
|
|
28
|
-
|
|
32
|
+
draftFromShare,
|
|
29
33
|
useSaveAgentShare,
|
|
30
34
|
type AgentShareDraft,
|
|
31
35
|
type SharingAudience,
|
|
@@ -53,6 +57,24 @@ export interface ShareAgentDialogProps {
|
|
|
53
57
|
readonly onOpenChange: (open: boolean) => void;
|
|
54
58
|
/** The agent whose sharing is managed. */
|
|
55
59
|
readonly agent: Agent;
|
|
60
|
+
/**
|
|
61
|
+
* The exact share to edit. When omitted, the dialog opens in **create
|
|
62
|
+
* mode**: a name/slug step creates a new share in `shareOrg`, then the
|
|
63
|
+
* dialog becomes its editor. The share's identity is immutable once
|
|
64
|
+
* created (decision 011 D2) — edit mode never renames.
|
|
65
|
+
*/
|
|
66
|
+
readonly share?: AgentShare | null;
|
|
67
|
+
/**
|
|
68
|
+
* The org that owns (and pays for) a share created by this dialog.
|
|
69
|
+
* Defaults to the agent's own org — the owner's channel. Pass the
|
|
70
|
+
* viewer's org to create a **cross-org share** of another org's
|
|
71
|
+
* marketplace-public agent (decision 013): the share, its billing,
|
|
72
|
+
* its credentials, and its hosted URL all belong to this org, while
|
|
73
|
+
* the agent stays live in its own. Cross-org shares are
|
|
74
|
+
* public-audience only, so the audience selector is hidden in that
|
|
75
|
+
* mode. Ignored in edit mode — an existing share knows its org.
|
|
76
|
+
*/
|
|
77
|
+
readonly shareOrg?: string;
|
|
56
78
|
/**
|
|
57
79
|
* Builds the absolute public chat URL for the shared agent. The host
|
|
58
80
|
* application owns URL construction (its configured public origin may
|
|
@@ -62,19 +84,9 @@ export interface ShareAgentDialogProps {
|
|
|
62
84
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
63
85
|
/**
|
|
64
86
|
* Called after any sharing change is persisted. Hosts typically pass
|
|
65
|
-
* the
|
|
87
|
+
* the share list's `refetch`.
|
|
66
88
|
*/
|
|
67
89
|
readonly onSharingChanged?: () => void;
|
|
68
|
-
/**
|
|
69
|
-
* The org that owns (and pays for) the share. Defaults to the agent's
|
|
70
|
-
* own org — the owner's channel. Pass the viewer's org to manage a
|
|
71
|
-
* **cross-org share** of another org's marketplace-public agent
|
|
72
|
-
* (decision 013): the share, its billing, its credentials, and its
|
|
73
|
-
* hosted URL all belong to this org, while the agent stays live in its
|
|
74
|
-
* own. Cross-org shares are public-audience only, so the audience
|
|
75
|
-
* selector is hidden in that mode.
|
|
76
|
-
*/
|
|
77
|
-
readonly shareOrg?: string;
|
|
78
90
|
/**
|
|
79
91
|
* When `false`, renders as an in-flow open dialog instead of a
|
|
80
92
|
* top-layer modal — no `showModal()`, no backdrop, no focus trap.
|
|
@@ -86,37 +98,42 @@ export interface ShareAgentDialogProps {
|
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
/**
|
|
89
|
-
* The Share dialog for
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* and discover the PlatformClient
|
|
101
|
+
* The Share dialog for one agent share: create it (name/slug step), then
|
|
102
|
+
* toggle serving, copy the hosted chat link, copy an embeddable snippet,
|
|
103
|
+
* manage allowed embed origins, bind tool credentials for visitors,
|
|
104
|
+
* customize visitor refusal messages, and discover the PlatformClient
|
|
105
|
+
* SDK path.
|
|
93
106
|
*
|
|
94
107
|
* Sharing is a distinct consent from marketplace visibility: visibility
|
|
95
108
|
* governs who can *read* the blueprint; sharing governs who can *chat*
|
|
96
|
-
* with the running agent — billed to the
|
|
97
|
-
* who pays next to the toggle so
|
|
109
|
+
* with the running agent — billed to the org that owns the share. The
|
|
110
|
+
* dialog states who pays before creation and next to the toggle, so
|
|
111
|
+
* sharing never surprises.
|
|
98
112
|
*
|
|
99
|
-
* Sharing lives in its own **AgentShare resource** (decision 011),
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
113
|
+
* Sharing lives in its own **AgentShare resource** (decision 011), and
|
|
114
|
+
* an agent can carry N shares — each its own channel with its own URL,
|
|
115
|
+
* audience, origins, credentials, and link token (D3). The dialog edits
|
|
116
|
+
* exactly the share it is given; it never resolves "the" share of an
|
|
117
|
+
* agent. Every save is an idempotent `apply` of the complete
|
|
118
|
+
* configuration ({@link useSaveAgentShare}), and the local draft is
|
|
119
|
+
* refreshed from every returned share, so the dialog never drifts from
|
|
120
|
+
* the server.
|
|
105
121
|
*
|
|
106
122
|
* Built on the native `<dialog>` element for focus trapping and escape
|
|
107
123
|
* handling, matching the SDK's modal convention ({@link ManageAccessDialog}).
|
|
108
124
|
* All visual properties flow through `--stgm-*` design tokens.
|
|
109
125
|
*
|
|
110
|
-
* Most hosts mount it via {@link
|
|
126
|
+
* Most hosts mount it via {@link AgentShareList} (the Shares tab).
|
|
111
127
|
* Render it directly only when you own the open-state.
|
|
112
128
|
*/
|
|
113
129
|
export function ShareAgentDialog({
|
|
114
130
|
open,
|
|
115
131
|
onOpenChange,
|
|
116
132
|
agent,
|
|
133
|
+
share,
|
|
134
|
+
shareOrg,
|
|
117
135
|
buildShareUrl,
|
|
118
136
|
onSharingChanged,
|
|
119
|
-
shareOrg,
|
|
120
137
|
modal = true,
|
|
121
138
|
}: ShareAgentDialogProps) {
|
|
122
139
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
@@ -154,14 +171,15 @@ export function ShareAgentDialog({
|
|
|
154
171
|
)}
|
|
155
172
|
aria-labelledby="share-agent-title"
|
|
156
173
|
>
|
|
157
|
-
{/* Body mounts only while open so
|
|
158
|
-
|
|
174
|
+
{/* Body mounts only while open so its draft state resets per
|
|
175
|
+
session — reopening the dialog never shows a stale draft. */}
|
|
159
176
|
{open && (
|
|
160
177
|
<ShareAgentDialogBody
|
|
161
178
|
agent={agent}
|
|
179
|
+
share={share ?? null}
|
|
180
|
+
shareOrg={shareOrg}
|
|
162
181
|
buildShareUrl={buildShareUrl}
|
|
163
182
|
onSharingChanged={onSharingChanged}
|
|
164
|
-
shareOrg={shareOrg}
|
|
165
183
|
onClose={handleClose}
|
|
166
184
|
/>
|
|
167
185
|
)}
|
|
@@ -170,31 +188,39 @@ export function ShareAgentDialog({
|
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
// ---------------------------------------------------------------------------
|
|
173
|
-
// Dialog body —
|
|
191
|
+
// Dialog body — create step or editor, depending on the share it was given
|
|
174
192
|
// ---------------------------------------------------------------------------
|
|
175
193
|
|
|
176
194
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
195
|
+
* Renders the create step (no share yet) or the editor (share in hand).
|
|
196
|
+
* A share persisted by the create step is adopted locally, so one dialog
|
|
197
|
+
* session flows create → edit without remounting — and the editor's
|
|
198
|
+
* draft seeding stays synchronous (`useState` initializer from the
|
|
199
|
+
* share): no hydrate-on-effect, no window where the switch shows a state
|
|
200
|
+
* the server never had.
|
|
181
201
|
*/
|
|
182
202
|
function ShareAgentDialogBody({
|
|
183
203
|
agent,
|
|
204
|
+
share,
|
|
205
|
+
shareOrg,
|
|
184
206
|
buildShareUrl,
|
|
185
207
|
onSharingChanged,
|
|
186
|
-
shareOrg,
|
|
187
208
|
onClose,
|
|
188
209
|
}: {
|
|
189
210
|
readonly agent: Agent;
|
|
211
|
+
readonly share: AgentShare | null;
|
|
212
|
+
readonly shareOrg?: string;
|
|
190
213
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
191
214
|
readonly onSharingChanged?: () => void;
|
|
192
|
-
readonly shareOrg?: string;
|
|
193
215
|
readonly onClose: () => void;
|
|
194
216
|
}) {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
217
|
+
const [created, setCreated] = useState<AgentShare | null>(null);
|
|
218
|
+
const activeShare = share ?? created;
|
|
219
|
+
const isCreating = activeShare === null;
|
|
220
|
+
|
|
221
|
+
const resolvedShareOrg =
|
|
222
|
+
activeShare?.metadata?.org || shareOrg || (agent.metadata?.org ?? "");
|
|
223
|
+
const isCrossOrg = resolvedShareOrg !== (agent.metadata?.org ?? "");
|
|
198
224
|
|
|
199
225
|
return (
|
|
200
226
|
<div className="flex flex-col">
|
|
@@ -205,7 +231,7 @@ function ShareAgentDialogBody({
|
|
|
205
231
|
id="share-agent-title"
|
|
206
232
|
className="text-base font-semibold text-foreground"
|
|
207
233
|
>
|
|
208
|
-
Share
|
|
234
|
+
{isCreating ? "Create share" : "Share"}
|
|
209
235
|
</h2>
|
|
210
236
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
211
237
|
{/* Cross-org: qualify the agent so it's clear whose blueprint
|
|
@@ -229,42 +255,21 @@ function ShareAgentDialogBody({
|
|
|
229
255
|
</button>
|
|
230
256
|
</div>
|
|
231
257
|
|
|
232
|
-
{
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
) : error ? (
|
|
242
|
-
<div className="px-6 py-8 text-center" role="alert">
|
|
243
|
-
<p className="text-sm text-foreground">
|
|
244
|
-
Couldn't load this agent's sharing settings.
|
|
245
|
-
</p>
|
|
246
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
247
|
-
{getUserMessage(error)}
|
|
248
|
-
</p>
|
|
249
|
-
<button
|
|
250
|
-
type="button"
|
|
251
|
-
onClick={refetch}
|
|
252
|
-
className={cn(
|
|
253
|
-
"mt-3 rounded-md px-3 py-1.5 text-xs font-medium",
|
|
254
|
-
"border border-border text-foreground hover:bg-accent-hover",
|
|
255
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
256
|
-
)}
|
|
257
|
-
>
|
|
258
|
-
Try again
|
|
259
|
-
</button>
|
|
260
|
-
</div>
|
|
258
|
+
{isCreating ? (
|
|
259
|
+
<CreateShareForm
|
|
260
|
+
agent={agent}
|
|
261
|
+
shareOrg={resolvedShareOrg}
|
|
262
|
+
onCreated={(persisted) => {
|
|
263
|
+
setCreated(persisted);
|
|
264
|
+
onSharingChanged?.();
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
261
267
|
) : (
|
|
262
268
|
<ShareAgentForm
|
|
263
269
|
agent={agent}
|
|
264
|
-
initialShare={
|
|
270
|
+
initialShare={activeShare}
|
|
265
271
|
buildShareUrl={buildShareUrl}
|
|
266
272
|
onSharingChanged={onSharingChanged}
|
|
267
|
-
shareOrg={shareOrg}
|
|
268
273
|
/>
|
|
269
274
|
)}
|
|
270
275
|
|
|
@@ -275,11 +280,13 @@ function ShareAgentDialogBody({
|
|
|
275
280
|
onClick={onClose}
|
|
276
281
|
className={cn(
|
|
277
282
|
"rounded-md px-3 py-1.5 text-sm font-medium",
|
|
278
|
-
|
|
283
|
+
isCreating
|
|
284
|
+
? "border border-border text-foreground hover:bg-accent-hover"
|
|
285
|
+
: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
279
286
|
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
280
287
|
)}
|
|
281
288
|
>
|
|
282
|
-
Done
|
|
289
|
+
{isCreating ? "Cancel" : "Done"}
|
|
283
290
|
</button>
|
|
284
291
|
</div>
|
|
285
292
|
</div>
|
|
@@ -287,71 +294,262 @@ function ShareAgentDialogBody({
|
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
// ---------------------------------------------------------------------------
|
|
290
|
-
//
|
|
297
|
+
// Create step — the new share's identity and cost, before any channel exists
|
|
291
298
|
// ---------------------------------------------------------------------------
|
|
292
299
|
|
|
293
300
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
301
|
+
* Names the new share and creates it live (`enabled: true`, public
|
|
302
|
+
* audience — the server's own defaults). Identity is set here because it
|
|
303
|
+
* is immutable afterward (decision 011 D2): the slug becomes the hosted
|
|
304
|
+
* URL `/chat/<org>/<slug>` in the sharing org's namespace.
|
|
305
|
+
*
|
|
306
|
+
* The slug auto-derives from the name until the user edits it (the
|
|
307
|
+
* {@link CreateOrganizationForm} pattern), validated by the same
|
|
308
|
+
* protovalidate rules the server enforces. An `(org, slug)` collision —
|
|
309
|
+
* the server's ALREADY_EXISTS — surfaces inline on the slug field with a
|
|
310
|
+
* pick-another-slug remedy instead of a generic failure toast.
|
|
298
311
|
*/
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
function CreateShareForm({
|
|
313
|
+
agent,
|
|
314
|
+
shareOrg,
|
|
315
|
+
onCreated,
|
|
316
|
+
}: {
|
|
317
|
+
readonly agent: Agent;
|
|
318
|
+
readonly shareOrg: string;
|
|
319
|
+
readonly onCreated: (share: AgentShare) => void;
|
|
320
|
+
}) {
|
|
321
|
+
const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
|
|
322
|
+
const isCrossOrg = shareOrg !== (agent.metadata?.org ?? "");
|
|
323
|
+
|
|
324
|
+
const [name, setName] = useState(agentName);
|
|
325
|
+
const [slug, setSlug] = useState(agent.metadata?.slug ?? "");
|
|
326
|
+
// Once the user manually edits the slug field, stop auto-deriving.
|
|
327
|
+
const slugTouchedRef = useRef(false);
|
|
328
|
+
// A server-side (org, slug) collision pins to the slug field until the
|
|
329
|
+
// slug changes — the remedy is picking another slug.
|
|
330
|
+
const [collisionError, setCollisionError] = useState<string | null>(null);
|
|
331
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
332
|
+
|
|
333
|
+
const { save, isPending } = useSaveAgentShare(agent, shareOrg);
|
|
334
|
+
|
|
335
|
+
const trimmedName = name.trim();
|
|
336
|
+
const violations =
|
|
337
|
+
slug.length > 0 || trimmedName.length > 0
|
|
338
|
+
? validateMessage(
|
|
339
|
+
ApiResourceMetadataSchema,
|
|
340
|
+
createMessage(ApiResourceMetadataSchema, {
|
|
341
|
+
...(slug.length > 0 && { slug }),
|
|
342
|
+
...(trimmedName.length > 0 && { name: trimmedName }),
|
|
343
|
+
}),
|
|
344
|
+
)
|
|
345
|
+
: [];
|
|
346
|
+
const slugError =
|
|
347
|
+
collisionError ?? (slug.length > 0 ? getFieldError(violations, "slug") : null);
|
|
348
|
+
const nameError =
|
|
349
|
+
trimmedName.length > 0 ? getFieldError(violations, "name") : null;
|
|
350
|
+
const canSubmit =
|
|
351
|
+
trimmedName !== "" && slug.length > 0 && slugError === null &&
|
|
352
|
+
nameError === null && !isPending;
|
|
353
|
+
|
|
354
|
+
const handleNameChange = useCallback((value: string) => {
|
|
355
|
+
setName(value);
|
|
356
|
+
if (!slugTouchedRef.current) {
|
|
357
|
+
setSlug(generateSlug(value.trim()));
|
|
358
|
+
setCollisionError(null);
|
|
359
|
+
}
|
|
360
|
+
}, []);
|
|
361
|
+
|
|
362
|
+
const handleSlugChange = useCallback((value: string) => {
|
|
363
|
+
slugTouchedRef.current = true;
|
|
364
|
+
setSlug(value);
|
|
365
|
+
setCollisionError(null);
|
|
366
|
+
}, []);
|
|
367
|
+
|
|
368
|
+
const handleSubmit = useCallback(
|
|
369
|
+
async (e: FormEvent) => {
|
|
370
|
+
e.preventDefault();
|
|
371
|
+
if (!canSubmit) return;
|
|
372
|
+
setSubmitError(null);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const persisted = await save(
|
|
376
|
+
// The server's create defaults, made explicit: live from the
|
|
377
|
+
// start (the user's one intent-click is "share it"), public
|
|
378
|
+
// audience, nothing else configured yet.
|
|
379
|
+
{ ...draftFromShare(null), enabled: true },
|
|
380
|
+
null,
|
|
381
|
+
{ name: trimmedName, slug },
|
|
382
|
+
);
|
|
383
|
+
if (persisted) {
|
|
384
|
+
toast.success("Share created — the link is live");
|
|
385
|
+
onCreated(persisted);
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (err instanceof StigmerError && err.code === "already-exists") {
|
|
389
|
+
setCollisionError(
|
|
390
|
+
`"${slug}" is already used by another share or resource in ${shareOrg} — pick a different slug`,
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
// e.g. the cross-org FAILED_PRECONDITION naming non-public
|
|
394
|
+
// dependencies (decision 013 D5) — show the server's words.
|
|
395
|
+
setSubmitError(getUserMessage(err));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
309
398
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
399
|
+
[canSubmit, save, trimmedName, slug, shareOrg, onCreated],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4 px-6 py-4">
|
|
404
|
+
<p className="text-xs text-muted-foreground">
|
|
405
|
+
A share is its own channel to this agent: a hosted chat link with
|
|
406
|
+
its own audience, embed origins, tool credentials, and visitor
|
|
407
|
+
messages.{" "}
|
|
408
|
+
{isCrossOrg && (
|
|
409
|
+
<>
|
|
410
|
+
This one lives in your organization{" "}
|
|
411
|
+
<span className="font-medium">{shareOrg}</span> while the agent
|
|
412
|
+
stays in <span className="font-medium">{agent.metadata?.org}</span>{" "}
|
|
413
|
+
— updates to the agent apply live.{" "}
|
|
414
|
+
</>
|
|
415
|
+
)}
|
|
416
|
+
Visitors chat on <span className="font-medium">{shareOrg}</span>'s
|
|
417
|
+
credits.
|
|
418
|
+
</p>
|
|
419
|
+
|
|
420
|
+
{/* ---- Name ---- */}
|
|
421
|
+
<div className="space-y-1">
|
|
422
|
+
<label
|
|
423
|
+
htmlFor="stgm-new-share-name"
|
|
424
|
+
className="text-xs font-medium text-foreground"
|
|
425
|
+
>
|
|
426
|
+
Name
|
|
427
|
+
</label>
|
|
428
|
+
<input
|
|
429
|
+
id="stgm-new-share-name"
|
|
430
|
+
type="text"
|
|
431
|
+
value={name}
|
|
432
|
+
onChange={(e) => handleNameChange(e.target.value)}
|
|
433
|
+
disabled={isPending}
|
|
434
|
+
required
|
|
435
|
+
maxLength={63}
|
|
436
|
+
className={cn(
|
|
437
|
+
"w-full rounded-md border bg-input-bg px-2.5 py-1.5 text-xs text-foreground",
|
|
438
|
+
"placeholder:text-muted-foreground",
|
|
439
|
+
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
440
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
441
|
+
nameError ? "border-destructive" : "border-border",
|
|
442
|
+
)}
|
|
443
|
+
/>
|
|
444
|
+
{nameError ? (
|
|
445
|
+
<p className="text-[0.65rem] text-destructive" role="alert">
|
|
446
|
+
{nameError}
|
|
447
|
+
</p>
|
|
448
|
+
) : (
|
|
449
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
450
|
+
A display name for this share — e.g. the site or campaign it
|
|
451
|
+
serves.
|
|
452
|
+
</p>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* ---- Slug ---- */}
|
|
457
|
+
<div className="space-y-1">
|
|
458
|
+
<label
|
|
459
|
+
htmlFor="stgm-new-share-slug"
|
|
460
|
+
className="text-xs font-medium text-foreground"
|
|
461
|
+
>
|
|
462
|
+
Slug
|
|
463
|
+
</label>
|
|
464
|
+
<input
|
|
465
|
+
id="stgm-new-share-slug"
|
|
466
|
+
type="text"
|
|
467
|
+
value={slug}
|
|
468
|
+
onChange={(e) => handleSlugChange(e.target.value)}
|
|
469
|
+
disabled={isPending}
|
|
470
|
+
required
|
|
471
|
+
className={cn(
|
|
472
|
+
"w-full rounded-md border bg-input-bg px-2.5 py-1.5 font-mono text-xs text-foreground",
|
|
473
|
+
"placeholder:text-muted-foreground",
|
|
474
|
+
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
475
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
476
|
+
slugError ? "border-destructive" : "border-border",
|
|
477
|
+
)}
|
|
478
|
+
/>
|
|
479
|
+
{slugError ? (
|
|
480
|
+
<p className="text-[0.65rem] text-destructive" role="alert">
|
|
481
|
+
{slugError}
|
|
482
|
+
</p>
|
|
483
|
+
) : (
|
|
484
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
485
|
+
Becomes the share's address:{" "}
|
|
486
|
+
<code className="font-mono">
|
|
487
|
+
/chat/{shareOrg}/{slug || "…"}
|
|
488
|
+
</code>
|
|
489
|
+
. Can't be changed later.
|
|
490
|
+
</p>
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{submitError && (
|
|
495
|
+
<p className="text-xs text-destructive" role="alert">
|
|
496
|
+
{submitError}
|
|
497
|
+
</p>
|
|
498
|
+
)}
|
|
499
|
+
|
|
500
|
+
<div className="flex items-center justify-end">
|
|
501
|
+
<button
|
|
502
|
+
type="submit"
|
|
503
|
+
disabled={!canSubmit}
|
|
504
|
+
className={cn(
|
|
505
|
+
"rounded-md px-3 py-1.5 text-sm font-medium",
|
|
506
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
507
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
508
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
509
|
+
)}
|
|
510
|
+
>
|
|
511
|
+
{isPending ? "Creating…" : "Create share"}
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
</form>
|
|
515
|
+
);
|
|
315
516
|
}
|
|
316
517
|
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Form — owns the share draft for the session
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
317
522
|
function ShareAgentForm({
|
|
318
523
|
agent,
|
|
319
524
|
initialShare,
|
|
320
525
|
buildShareUrl,
|
|
321
526
|
onSharingChanged,
|
|
322
|
-
shareOrg,
|
|
323
527
|
}: {
|
|
324
528
|
readonly agent: Agent;
|
|
325
|
-
readonly initialShare: AgentShare
|
|
529
|
+
readonly initialShare: AgentShare;
|
|
326
530
|
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
327
531
|
readonly onSharingChanged?: () => void;
|
|
328
|
-
readonly shareOrg?: string;
|
|
329
532
|
}) {
|
|
330
533
|
const agentName = agent.metadata?.name || (agent.metadata?.slug ?? "");
|
|
331
534
|
|
|
332
535
|
// The latest server share is the single baseline: the draft, the link
|
|
333
|
-
// token, and the share id for rotation all derive from it.
|
|
334
|
-
|
|
335
|
-
const [share, setShare] = useState<AgentShare | null>(initialShare);
|
|
536
|
+
// token, and the share id for rotation all derive from it.
|
|
537
|
+
const [share, setShare] = useState<AgentShare>(initialShare);
|
|
336
538
|
const [draft, setDraft] = useState<AgentShareDraft>(() =>
|
|
337
539
|
draftFromShare(initialShare),
|
|
338
540
|
);
|
|
339
541
|
const [activeTab, setActiveTab] = useState("link");
|
|
340
542
|
|
|
341
|
-
const { save, isPending } = useSaveAgentShare(agent
|
|
543
|
+
const { save, isPending } = useSaveAgentShare(agent);
|
|
342
544
|
const { rotateShareLink, isPending: isRotating } = useRotateShareLink(
|
|
343
|
-
share
|
|
545
|
+
share.metadata?.id ?? null,
|
|
344
546
|
);
|
|
345
547
|
|
|
346
|
-
// The share's own org/slug
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const org =
|
|
352
|
-
share?.metadata?.org || shareOrg || (agent.metadata?.org ?? "");
|
|
353
|
-
const slug = share?.metadata?.slug || (agent.metadata?.slug ?? "");
|
|
354
|
-
const linkToken = share?.status?.shareLinkToken ?? "";
|
|
548
|
+
// The share's own org/slug are the hosted URL — for a cross-org share
|
|
549
|
+
// (decision 013) that org is the sharing org's, not the agent's.
|
|
550
|
+
const org = share.metadata?.org ?? "";
|
|
551
|
+
const slug = share.metadata?.slug ?? "";
|
|
552
|
+
const linkToken = share.status?.shareLinkToken ?? "";
|
|
355
553
|
// Cross-org shares are public-audience only (decision 013 D3) — the
|
|
356
554
|
// audience selector disappears rather than offering a choice the
|
|
357
555
|
// server would refuse.
|