@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
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import {
|
|
6
|
+
appendLinkToken,
|
|
7
|
+
chatPath,
|
|
8
|
+
getUserMessage,
|
|
9
|
+
} from "@stigmer/sdk";
|
|
10
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
11
|
+
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
12
|
+
import { toast } from "../feedback/toast.js";
|
|
13
|
+
import { PermissionGate } from "../iam-policy/PermissionGate.js";
|
|
14
|
+
import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
|
|
15
|
+
import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
|
|
16
|
+
import { useCopyResource } from "../resource-detail/useCopyResource.js";
|
|
17
|
+
import { ShareAgentDialog } from "./ShareAgentDialog.js";
|
|
18
|
+
import { useAgentShares } from "./useAgentShares.js";
|
|
19
|
+
import { useCanCreateAgentShare } from "./useCanCreateAgentShare.js";
|
|
20
|
+
import { useDeleteAgentShare } from "./useDeleteAgentShare.js";
|
|
21
|
+
import { useRotateShareLink } from "./useRotateShareLink.js";
|
|
22
|
+
import {
|
|
23
|
+
draftFromShare,
|
|
24
|
+
sharingAudienceFromProto,
|
|
25
|
+
useSaveAgentShare,
|
|
26
|
+
} from "./useSaveAgentShare.js";
|
|
27
|
+
|
|
28
|
+
/** Props for {@link AgentShareList}. */
|
|
29
|
+
export interface AgentShareListProps {
|
|
30
|
+
/** The agent whose shares are managed. */
|
|
31
|
+
readonly agent: Agent;
|
|
32
|
+
/**
|
|
33
|
+
* The viewer's active organization slug. A share created from this
|
|
34
|
+
* list lands in this org — its URL, billing, and credentials belong
|
|
35
|
+
* to it (a **cross-org share** when it differs from the agent's org,
|
|
36
|
+
* decision 013). Omit to default to the agent's own org.
|
|
37
|
+
*/
|
|
38
|
+
readonly viewerOrg?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Builds the absolute public chat URL for a share. The host
|
|
41
|
+
* application owns URL construction (its configured public origin may
|
|
42
|
+
* differ from the rendering origin — e.g. the desktop app). When
|
|
43
|
+
* omitted, links fall back to the relative `/chat/<org>/<slug>`.
|
|
44
|
+
*/
|
|
45
|
+
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
46
|
+
/** Additional CSS class names. */
|
|
47
|
+
readonly className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Management surface for an agent's {@link AgentShare} channels — the
|
|
52
|
+
* agent analog of {@link AgentInstanceList}, rendered in the agent
|
|
53
|
+
* detail view's Shares tab.
|
|
54
|
+
*
|
|
55
|
+
* Lists every share the viewer can see (the server FGA-filters
|
|
56
|
+
* `getByAgent`, so each org's members see their own org's channels and
|
|
57
|
+
* never another org's — decision 013), with per-row copy link, edit,
|
|
58
|
+
* pause/resume, reset link, and delete. Creation goes through the same
|
|
59
|
+
* {@link ShareAgentDialog} and always lands in the viewer's active org;
|
|
60
|
+
* the create bar mirrors the server's via
|
|
61
|
+
* {@link useCanCreateAgentShare}, so the button never appears to a user
|
|
62
|
+
* whose create would be refused.
|
|
63
|
+
*
|
|
64
|
+
* Self-contained: owns its dialog, its confirmation prompts, and its
|
|
65
|
+
* refetch-after-mutation — hosts render it with just the agent and the
|
|
66
|
+
* URL builder.
|
|
67
|
+
*
|
|
68
|
+
* This is an SDK component (DD-001) — embeddable by platform builders.
|
|
69
|
+
*/
|
|
70
|
+
export function AgentShareList({
|
|
71
|
+
agent,
|
|
72
|
+
viewerOrg,
|
|
73
|
+
buildShareUrl,
|
|
74
|
+
className,
|
|
75
|
+
}: AgentShareListProps) {
|
|
76
|
+
const agentId = agent.metadata?.id ?? "";
|
|
77
|
+
const { shares, isLoading, error, refetch } = useAgentShares(agentId);
|
|
78
|
+
const { allowed: canCreate, shareOrg } = useCanCreateAgentShare(
|
|
79
|
+
agent,
|
|
80
|
+
viewerOrg,
|
|
81
|
+
);
|
|
82
|
+
const { deleteShare } = useDeleteAgentShare();
|
|
83
|
+
const { confirmState, confirm, handleConfirm, handleCancel } =
|
|
84
|
+
useConfirmAction();
|
|
85
|
+
|
|
86
|
+
// One dialog instance serves both flows: editing the chosen share, or
|
|
87
|
+
// creating a new one in the viewer's org.
|
|
88
|
+
const [editor, setEditor] = useState<
|
|
89
|
+
| { readonly mode: "create" }
|
|
90
|
+
| { readonly mode: "edit"; readonly share: AgentShare }
|
|
91
|
+
| null
|
|
92
|
+
>(null);
|
|
93
|
+
|
|
94
|
+
const handleDelete = useCallback(
|
|
95
|
+
async (share: AgentShare) => {
|
|
96
|
+
const slug = share.metadata?.slug ?? "";
|
|
97
|
+
const confirmed = await confirm({
|
|
98
|
+
title: "Delete share?",
|
|
99
|
+
description:
|
|
100
|
+
`The link /chat/${share.metadata?.org}/${slug} stops working ` +
|
|
101
|
+
"immediately — including for visitors mid-conversation — and its " +
|
|
102
|
+
"configuration (origins, messages, credential bindings) is gone. " +
|
|
103
|
+
"To stop serving while keeping the configuration, pause it instead.",
|
|
104
|
+
confirmLabel: "Delete",
|
|
105
|
+
variant: "destructive",
|
|
106
|
+
});
|
|
107
|
+
if (!confirmed) return;
|
|
108
|
+
try {
|
|
109
|
+
await deleteShare(share.metadata?.id ?? "");
|
|
110
|
+
toast.success("Share deleted");
|
|
111
|
+
refetch();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
toast.error(getUserMessage(err));
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[confirm, deleteShare, refetch],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (isLoading) {
|
|
120
|
+
return <LoadingSkeleton />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="py-8 text-center text-sm text-destructive">
|
|
126
|
+
Failed to load shares
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className={cn("space-y-3", className)}>
|
|
133
|
+
{shares.length === 0 ? (
|
|
134
|
+
<ShareEmptyState
|
|
135
|
+
canCreate={canCreate}
|
|
136
|
+
onCreateClick={() => setEditor({ mode: "create" })}
|
|
137
|
+
/>
|
|
138
|
+
) : (
|
|
139
|
+
<>
|
|
140
|
+
<div className="flex items-center justify-between">
|
|
141
|
+
<h3 className="text-sm font-medium text-foreground">
|
|
142
|
+
{shares.length} {shares.length === 1 ? "share" : "shares"}
|
|
143
|
+
</h3>
|
|
144
|
+
{canCreate && (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setEditor({ mode: "create" })}
|
|
148
|
+
className={cn(
|
|
149
|
+
"inline-flex items-center gap-1 rounded-md px-2.5 py-1",
|
|
150
|
+
"text-xs font-medium",
|
|
151
|
+
"border border-border text-foreground",
|
|
152
|
+
"hover:bg-accent-hover",
|
|
153
|
+
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
<PlusIcon />
|
|
157
|
+
Create share
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
163
|
+
<table className="w-full text-sm">
|
|
164
|
+
<thead>
|
|
165
|
+
<tr className="border-b border-border bg-muted-subtle">
|
|
166
|
+
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Name</th>
|
|
167
|
+
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Link</th>
|
|
168
|
+
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Audience</th>
|
|
169
|
+
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Status</th>
|
|
170
|
+
<th className="px-4 py-2 text-right font-medium text-muted-foreground">Actions</th>
|
|
171
|
+
</tr>
|
|
172
|
+
</thead>
|
|
173
|
+
<tbody className="divide-y divide-border">
|
|
174
|
+
{shares.map((share) => (
|
|
175
|
+
<ShareRow
|
|
176
|
+
key={share.metadata?.id}
|
|
177
|
+
share={share}
|
|
178
|
+
agent={agent}
|
|
179
|
+
buildShareUrl={buildShareUrl}
|
|
180
|
+
onEditClick={(s) => setEditor({ mode: "edit", share: s })}
|
|
181
|
+
onDeleteClick={handleDelete}
|
|
182
|
+
refetch={refetch}
|
|
183
|
+
/>
|
|
184
|
+
))}
|
|
185
|
+
</tbody>
|
|
186
|
+
</table>
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{editor && (
|
|
192
|
+
<ShareAgentDialog
|
|
193
|
+
open
|
|
194
|
+
onOpenChange={(open) => {
|
|
195
|
+
if (!open) setEditor(null);
|
|
196
|
+
}}
|
|
197
|
+
agent={agent}
|
|
198
|
+
share={editor.mode === "edit" ? editor.share : undefined}
|
|
199
|
+
shareOrg={shareOrg}
|
|
200
|
+
buildShareUrl={buildShareUrl}
|
|
201
|
+
onSharingChanged={refetch}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<ConfirmDialog
|
|
206
|
+
state={confirmState}
|
|
207
|
+
onConfirm={handleConfirm}
|
|
208
|
+
onCancel={handleCancel}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Row — one share channel
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
interface ShareRowProps {
|
|
219
|
+
readonly share: AgentShare;
|
|
220
|
+
readonly agent: Agent;
|
|
221
|
+
readonly buildShareUrl?: (org: string, slug: string) => string;
|
|
222
|
+
readonly onEditClick: (share: AgentShare) => void;
|
|
223
|
+
readonly onDeleteClick: (share: AgentShare) => void;
|
|
224
|
+
readonly refetch: () => void;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ShareRow({
|
|
228
|
+
share,
|
|
229
|
+
agent,
|
|
230
|
+
buildShareUrl,
|
|
231
|
+
onEditClick,
|
|
232
|
+
onDeleteClick,
|
|
233
|
+
refetch,
|
|
234
|
+
}: ShareRowProps) {
|
|
235
|
+
const meta = share.metadata;
|
|
236
|
+
const id = meta?.id ?? "";
|
|
237
|
+
const org = meta?.org ?? "";
|
|
238
|
+
const slug = meta?.slug ?? "";
|
|
239
|
+
const enabled = share.spec?.enabled ?? false;
|
|
240
|
+
const audience = sharingAudienceFromProto(share.spec?.audience);
|
|
241
|
+
const isCrossOrg = org !== (agent.metadata?.org ?? "");
|
|
242
|
+
|
|
243
|
+
const { copy } = useCopyResource();
|
|
244
|
+
const { save, isPending } = useSaveAgentShare(agent);
|
|
245
|
+
const { rotateShareLink, isPending: isRotating } = useRotateShareLink(id);
|
|
246
|
+
|
|
247
|
+
// The copyable URL carries the link token on public shares (org
|
|
248
|
+
// audience is gated by membership, not the token). The display stays
|
|
249
|
+
// the bare path — the token is a secret, not an address.
|
|
250
|
+
const displayPath = chatPath(org, slug);
|
|
251
|
+
const copyUrl = (() => {
|
|
252
|
+
const base = buildShareUrl ? buildShareUrl(org, slug) : displayPath;
|
|
253
|
+
return audience === "org"
|
|
254
|
+
? base
|
|
255
|
+
: appendLinkToken(base, share.status?.shareLinkToken ?? "");
|
|
256
|
+
})();
|
|
257
|
+
|
|
258
|
+
const handleCopyLink = useCallback(() => {
|
|
259
|
+
void copy(copyUrl, "Link");
|
|
260
|
+
}, [copy, copyUrl]);
|
|
261
|
+
|
|
262
|
+
// Pause/resume is a full-spec save with only `enabled` flipped —
|
|
263
|
+
// draftFromShare guarantees a toggle can never wipe origins, messages,
|
|
264
|
+
// or credential bindings (the fails-closed posture the CLI shares).
|
|
265
|
+
const handleToggleEnabled = useCallback(async () => {
|
|
266
|
+
try {
|
|
267
|
+
await save({ ...draftFromShare(share), enabled: !enabled }, share);
|
|
268
|
+
toast.success(enabled ? "Share paused" : "Share resumed");
|
|
269
|
+
refetch();
|
|
270
|
+
} catch (err) {
|
|
271
|
+
toast.error(getUserMessage(err));
|
|
272
|
+
}
|
|
273
|
+
}, [save, share, enabled, refetch]);
|
|
274
|
+
|
|
275
|
+
const handleResetLink = useCallback(async () => {
|
|
276
|
+
try {
|
|
277
|
+
await rotateShareLink();
|
|
278
|
+
toast.success("Link reset — the old link no longer works");
|
|
279
|
+
refetch();
|
|
280
|
+
} catch (err) {
|
|
281
|
+
toast.error(getUserMessage(err));
|
|
282
|
+
}
|
|
283
|
+
}, [rotateShareLink, refetch]);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<tr
|
|
287
|
+
className="cursor-pointer transition-colors hover:bg-accent-hover"
|
|
288
|
+
onClick={() => onEditClick(share)}
|
|
289
|
+
>
|
|
290
|
+
<td className="px-4 py-2.5">
|
|
291
|
+
<div className="flex items-center gap-2">
|
|
292
|
+
<span className="font-medium text-foreground">
|
|
293
|
+
{meta?.name || slug || "\u2014"}
|
|
294
|
+
</span>
|
|
295
|
+
{isCrossOrg && (
|
|
296
|
+
<span
|
|
297
|
+
className={cn(
|
|
298
|
+
"inline-flex items-center rounded-md px-1.5 py-0.5",
|
|
299
|
+
"text-[0.6rem] font-medium uppercase tracking-wide",
|
|
300
|
+
"bg-muted text-muted-foreground border border-border",
|
|
301
|
+
)}
|
|
302
|
+
title={`This share lives in ${org}; the agent lives in ${agent.metadata?.org}`}
|
|
303
|
+
>
|
|
304
|
+
Cross-org
|
|
305
|
+
</span>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
</td>
|
|
309
|
+
|
|
310
|
+
<td className="px-4 py-2.5" onClick={(e) => e.stopPropagation()}>
|
|
311
|
+
<div className="flex items-center gap-1.5">
|
|
312
|
+
<code
|
|
313
|
+
className="max-w-56 truncate font-mono text-xs text-muted-foreground"
|
|
314
|
+
title={displayPath}
|
|
315
|
+
>
|
|
316
|
+
{displayPath}
|
|
317
|
+
</code>
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={handleCopyLink}
|
|
321
|
+
aria-label={`Copy link for ${meta?.name || slug}`}
|
|
322
|
+
className={cn(
|
|
323
|
+
"rounded p-1 text-muted-foreground",
|
|
324
|
+
"hover:bg-accent-hover hover:text-foreground",
|
|
325
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
<CopyIcon />
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
</td>
|
|
332
|
+
|
|
333
|
+
<td className="px-4 py-2.5">
|
|
334
|
+
<span className="text-xs text-muted-foreground">
|
|
335
|
+
{audience === "org" ? "Org members" : "Public"}
|
|
336
|
+
</span>
|
|
337
|
+
</td>
|
|
338
|
+
|
|
339
|
+
<td className="px-4 py-2.5">
|
|
340
|
+
<span
|
|
341
|
+
className={cn(
|
|
342
|
+
"inline-flex items-center gap-1.5 text-xs",
|
|
343
|
+
enabled ? "text-foreground" : "text-muted-foreground",
|
|
344
|
+
)}
|
|
345
|
+
>
|
|
346
|
+
<span
|
|
347
|
+
aria-hidden="true"
|
|
348
|
+
className={cn(
|
|
349
|
+
"size-1.5 rounded-full",
|
|
350
|
+
enabled ? "bg-success" : "bg-muted-foreground",
|
|
351
|
+
)}
|
|
352
|
+
/>
|
|
353
|
+
{enabled ? "Active" : "Paused"}
|
|
354
|
+
</span>
|
|
355
|
+
</td>
|
|
356
|
+
|
|
357
|
+
<td className="px-4 py-2.5 text-right" onClick={(e) => e.stopPropagation()}>
|
|
358
|
+
<div className="flex items-center justify-end gap-1">
|
|
359
|
+
<PermissionGate resource={{ kind: "agent_share", id }} relation="can_edit">
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
onClick={() => onEditClick(share)}
|
|
363
|
+
className={cn(
|
|
364
|
+
"rounded px-2 py-1 text-xs font-medium",
|
|
365
|
+
"text-foreground hover:bg-accent-hover",
|
|
366
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
367
|
+
)}
|
|
368
|
+
>
|
|
369
|
+
Edit
|
|
370
|
+
</button>
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
onClick={() => void handleToggleEnabled()}
|
|
374
|
+
disabled={isPending}
|
|
375
|
+
className={cn(
|
|
376
|
+
"rounded px-2 py-1 text-xs font-medium",
|
|
377
|
+
"text-foreground hover:bg-accent-hover",
|
|
378
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
379
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
{enabled ? "Pause" : "Resume"}
|
|
383
|
+
</button>
|
|
384
|
+
{audience === "public" && (
|
|
385
|
+
<button
|
|
386
|
+
type="button"
|
|
387
|
+
onClick={() => void handleResetLink()}
|
|
388
|
+
disabled={isRotating}
|
|
389
|
+
className={cn(
|
|
390
|
+
"rounded px-2 py-1 text-xs font-medium",
|
|
391
|
+
"text-foreground hover:bg-accent-hover",
|
|
392
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
393
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
394
|
+
)}
|
|
395
|
+
>
|
|
396
|
+
{isRotating ? "Resetting…" : "Reset link"}
|
|
397
|
+
</button>
|
|
398
|
+
)}
|
|
399
|
+
</PermissionGate>
|
|
400
|
+
<PermissionGate resource={{ kind: "agent_share", id }} relation="can_delete">
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={() => onDeleteClick(share)}
|
|
404
|
+
className={cn(
|
|
405
|
+
"rounded px-2 py-1 text-xs font-medium",
|
|
406
|
+
"text-destructive hover:bg-destructive-subtle",
|
|
407
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
408
|
+
)}
|
|
409
|
+
>
|
|
410
|
+
Delete
|
|
411
|
+
</button>
|
|
412
|
+
</PermissionGate>
|
|
413
|
+
</div>
|
|
414
|
+
</td>
|
|
415
|
+
</tr>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Empty state
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
function ShareEmptyState({
|
|
424
|
+
canCreate,
|
|
425
|
+
onCreateClick,
|
|
426
|
+
}: {
|
|
427
|
+
readonly canCreate: boolean;
|
|
428
|
+
readonly onCreateClick: () => void;
|
|
429
|
+
}) {
|
|
430
|
+
return (
|
|
431
|
+
<div className="flex flex-col items-center gap-2 py-12 text-center">
|
|
432
|
+
<ShareIcon className="size-10 text-muted-foreground-faint" />
|
|
433
|
+
<p className="text-sm font-medium text-muted-foreground">No shares yet</p>
|
|
434
|
+
<p className="max-w-sm text-xs text-muted-foreground-subtle">
|
|
435
|
+
A share gives this agent a hosted chat link you can send or embed —
|
|
436
|
+
each share is its own channel with its own audience, embed origins,
|
|
437
|
+
and tool credentials.
|
|
438
|
+
</p>
|
|
439
|
+
{canCreate && (
|
|
440
|
+
<button
|
|
441
|
+
type="button"
|
|
442
|
+
onClick={onCreateClick}
|
|
443
|
+
className={cn(
|
|
444
|
+
"mt-2 inline-flex items-center gap-1 rounded-md px-3 py-1.5",
|
|
445
|
+
"text-xs font-medium",
|
|
446
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
447
|
+
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
448
|
+
)}
|
|
449
|
+
>
|
|
450
|
+
<PlusIcon />
|
|
451
|
+
Create share
|
|
452
|
+
</button>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Non-happy states + icons
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
function LoadingSkeleton() {
|
|
463
|
+
return (
|
|
464
|
+
<div className="space-y-2 py-4">
|
|
465
|
+
{[1, 2, 3].map((i) => (
|
|
466
|
+
<div key={i} className="h-12 animate-pulse rounded-md bg-muted-faint" />
|
|
467
|
+
))}
|
|
468
|
+
</div>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function PlusIcon() {
|
|
473
|
+
return (
|
|
474
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
475
|
+
<path d="M6 2v8M2 6h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
476
|
+
</svg>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function CopyIcon() {
|
|
481
|
+
return (
|
|
482
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
483
|
+
<rect x="5.5" y="5.5" width="8" height="8" rx="1.5" />
|
|
484
|
+
<path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2" />
|
|
485
|
+
</svg>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ShareIcon({ className }: { readonly className?: string }) {
|
|
490
|
+
return (
|
|
491
|
+
<svg
|
|
492
|
+
className={className}
|
|
493
|
+
viewBox="0 0 16 16"
|
|
494
|
+
fill="none"
|
|
495
|
+
stroke="currentColor"
|
|
496
|
+
strokeWidth="1.5"
|
|
497
|
+
strokeLinecap="round"
|
|
498
|
+
strokeLinejoin="round"
|
|
499
|
+
aria-hidden="true"
|
|
500
|
+
>
|
|
501
|
+
<circle cx="4" cy="8" r="2" />
|
|
502
|
+
<circle cx="12" cy="4" r="2" />
|
|
503
|
+
<circle cx="12" cy="12" r="2" />
|
|
504
|
+
<path d="m5.8 7.1 4.4-2.2M5.8 8.9l4.4 2.2" />
|
|
505
|
+
</svg>
|
|
506
|
+
);
|
|
507
|
+
}
|