@agent-native/creative-context 0.3.0 → 0.5.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/README.md +51 -9
- package/agent-native.package.json +4 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-context-memberships.d.ts +11 -0
- package/dist/actions/list-context-memberships.d.ts.map +1 -0
- package/dist/actions/list-context-memberships.js +47 -0
- package/dist/actions/list-context-memberships.js.map +1 -0
- package/dist/actions/list-creative-contexts.d.ts +12 -0
- package/dist/actions/list-creative-contexts.d.ts.map +1 -0
- package/dist/actions/list-creative-contexts.js +28 -0
- package/dist/actions/list-creative-contexts.js.map +1 -0
- package/dist/actions/manage-context-membership.d.ts +64 -0
- package/dist/actions/manage-context-membership.d.ts.map +1 -0
- package/dist/actions/manage-context-membership.js +70 -0
- package/dist/actions/manage-context-membership.js.map +1 -0
- package/dist/actions/manage-context-pack.d.ts +3 -0
- package/dist/actions/manage-context-pack.js +6 -0
- package/dist/actions/manage-context-pack.js.map +1 -1
- package/dist/actions/manage-context-source.js +4 -11
- package/dist/actions/manage-context-source.js.map +1 -1
- package/dist/actions/manage-creative-context.d.ts +28 -0
- package/dist/actions/manage-creative-context.d.ts.map +1 -0
- package/dist/actions/manage-creative-context.js +65 -0
- package/dist/actions/manage-creative-context.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +16 -1
- package/dist/actions/process-context-purge.js +2 -1
- package/dist/actions/process-context-purge.js.map +1 -1
- package/dist/actions/search-creative-context.d.ts +1 -0
- package/dist/actions/search-creative-context.js +2 -1
- package/dist/actions/search-creative-context.js.map +1 -1
- package/dist/client/CreativeContextChip.d.ts +5 -1
- package/dist/client/CreativeContextChip.d.ts.map +1 -1
- package/dist/client/CreativeContextChip.js +36 -7
- package/dist/client/CreativeContextChip.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +406 -89
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/CreativeContextShareTab.d.ts +56 -0
- package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
- package/dist/client/CreativeContextShareTab.js +213 -0
- package/dist/client/CreativeContextShareTab.js.map +1 -0
- package/dist/client/actions.d.ts +210 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +318 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/application-state.d.ts +2 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +25 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +77 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/types.d.ts +2 -2
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1592 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +95 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +80 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/server/generation-artifact-access.js +2 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +12 -4
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +129 -7
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +3 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +3 -1
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/server/media.d.ts +1 -1
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +24 -9
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-resource-capture.d.ts +78 -0
- package/dist/server/native-resource-capture.d.ts.map +1 -0
- package/dist/server/native-resource-capture.js +94 -0
- package/dist/server/native-resource-capture.js.map +1 -0
- package/dist/server/public-serialization.js +1 -1
- package/dist/server/public-serialization.js.map +1 -1
- package/dist/server/retrieval.d.ts +1 -0
- package/dist/server/retrieval.d.ts.map +1 -1
- package/dist/server/retrieval.js +2 -0
- package/dist/server/retrieval.js.map +1 -1
- package/dist/server/safe-native-preview.d.ts +15 -0
- package/dist/server/safe-native-preview.d.ts.map +1 -0
- package/dist/server/safe-native-preview.js +149 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts.map +1 -1
- package/dist/store/brand.js +2 -16
- package/dist/store/brand.js.map +1 -1
- package/dist/store/content.d.ts +1 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +84 -20
- package/dist/store/content.js.map +1 -1
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1341 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/packs.d.ts +3 -0
- package/dist/store/packs.d.ts.map +1 -1
- package/dist/store/packs.js +126 -9
- package/dist/store/packs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +103 -6
- package/dist/store/purge.js.map +1 -1
- package/dist/store/sources.d.ts.map +1 -1
- package/dist/store/sources.js +2 -15
- package/dist/store/sources.js.map +1 -1
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +3 -1
- package/dist/store/suggestions.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +13 -5
- package/package.json +3 -3
- package/src/actions/index.ts +8 -0
- package/src/actions/job-public-serialization.spec.ts +21 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +6 -0
- package/src/actions/manage-context-source.ts +8 -11
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/process-context-purge.ts +4 -1
- package/src/actions/search-creative-context.ts +2 -1
- package/src/client/CreativeContextChip.spec.ts +32 -0
- package/src/client/CreativeContextChip.tsx +83 -14
- package/src/client/CreativeContextPanel.tsx +1947 -885
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +657 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +566 -0
- package/src/client/application-state.spec.ts +5 -0
- package/src/client/application-state.ts +30 -2
- package/src/client/index.ts +33 -0
- package/src/client/messages.ts +95 -0
- package/src/connectors/types.ts +6 -8
- package/src/eval/library-flow.acceptance.spec.tsx +1 -0
- package/src/schema/index.ts +116 -0
- package/src/schema/migrations.ts +80 -0
- package/src/server/generation-artifact-access.ts +1 -0
- package/src/server/generation-context-a2a.spec.ts +133 -0
- package/src/server/generation-context.ts +153 -7
- package/src/server/index.spec.ts +15 -0
- package/src/server/index.ts +16 -0
- package/src/server/isolated-a2a.ts +4 -1
- package/src/server/media.spec.ts +30 -2
- package/src/server/media.ts +31 -12
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/public-serialization.spec.ts +1 -0
- package/src/server/public-serialization.ts +1 -1
- package/src/server/retrieval.ts +3 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +200 -0
- package/src/store/access.integration.spec.ts +1116 -0
- package/src/store/brand.ts +2 -21
- package/src/store/content.ts +217 -17
- package/src/store/contexts.ts +1826 -0
- package/src/store/index.ts +1 -0
- package/src/store/packs.ts +279 -24
- package/src/store/purge.ts +139 -6
- package/src/store/sources.ts +2 -18
- package/src/store/suggestions.ts +3 -1
- package/src/types.ts +95 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Checkbox,
|
|
5
|
+
Input,
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
Sheet,
|
|
12
|
+
SheetContent,
|
|
13
|
+
SheetDescription,
|
|
14
|
+
SheetHeader,
|
|
15
|
+
SheetTitle,
|
|
16
|
+
Tabs,
|
|
17
|
+
TabsContent,
|
|
18
|
+
TabsList,
|
|
19
|
+
TabsTrigger,
|
|
20
|
+
Textarea,
|
|
21
|
+
} from "@agent-native/toolkit/ui";
|
|
22
|
+
import {
|
|
23
|
+
IconCheck,
|
|
24
|
+
IconFileText,
|
|
25
|
+
IconLink,
|
|
26
|
+
IconPlus,
|
|
27
|
+
IconShieldCheck,
|
|
28
|
+
IconX,
|
|
29
|
+
} from "@tabler/icons-react";
|
|
30
|
+
import { useEffect, useState } from "react";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
parseContextMembershipsForResource,
|
|
34
|
+
parseCreativeContexts,
|
|
35
|
+
useContextMemberships,
|
|
36
|
+
useCreativeContexts,
|
|
37
|
+
useManageContextMembership,
|
|
38
|
+
useManageCreativeContext,
|
|
39
|
+
type CreativeContextMembership,
|
|
40
|
+
type CreativeContextMembershipRank,
|
|
41
|
+
type CreativeContextPolicy,
|
|
42
|
+
type CreativeContextSummary,
|
|
43
|
+
} from "./actions.js";
|
|
44
|
+
|
|
45
|
+
export interface CreativeContextResourcePreview {
|
|
46
|
+
kind?: "image" | "document" | "text";
|
|
47
|
+
imageUrl?: string;
|
|
48
|
+
alt?: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CreativeContextResourceDescriptor {
|
|
53
|
+
appId: string;
|
|
54
|
+
resourceType: string;
|
|
55
|
+
resourceId: string;
|
|
56
|
+
title: string;
|
|
57
|
+
preview?: CreativeContextResourcePreview;
|
|
58
|
+
updatedAt?: string;
|
|
59
|
+
visibility?: "private" | "org" | "public";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CreativeContextShareTabProps {
|
|
63
|
+
resource?: CreativeContextResourceDescriptor;
|
|
64
|
+
resources?: readonly CreativeContextResourceDescriptor[];
|
|
65
|
+
canManage?: boolean;
|
|
66
|
+
className?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const MAX_CONTEXT_RESOURCES = 50;
|
|
70
|
+
|
|
71
|
+
export function normalizeCreativeContextResources(
|
|
72
|
+
resource?: CreativeContextResourceDescriptor,
|
|
73
|
+
resources?: readonly CreativeContextResourceDescriptor[],
|
|
74
|
+
): CreativeContextResourceDescriptor[] {
|
|
75
|
+
const candidates = resources?.length ? resources : resource ? [resource] : [];
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
return candidates.filter((candidate) => {
|
|
78
|
+
const key = `${candidate.appId}:${candidate.resourceType}:${candidate.resourceId}`;
|
|
79
|
+
if (seen.has(key) || seen.size >= MAX_CONTEXT_RESOURCES) return false;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const VISIBILITY_RANK = { private: 0, org: 1, public: 2 } as const;
|
|
86
|
+
|
|
87
|
+
export function requiresBroaderPublication(
|
|
88
|
+
resource: CreativeContextResourceDescriptor,
|
|
89
|
+
context: CreativeContextSummary | undefined,
|
|
90
|
+
) {
|
|
91
|
+
return Boolean(
|
|
92
|
+
context &&
|
|
93
|
+
VISIBILITY_RANK[context.visibility] >
|
|
94
|
+
VISIBILITY_RANK[resource.visibility ?? "private"],
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function creativeContextSafePreviewUrl(url: string | undefined) {
|
|
99
|
+
if (!url) return null;
|
|
100
|
+
try {
|
|
101
|
+
if (typeof window === "undefined") {
|
|
102
|
+
return new URL(url).protocol === "https:" ? url : null;
|
|
103
|
+
}
|
|
104
|
+
const parsed = new URL(url, window.location.origin);
|
|
105
|
+
return parsed.protocol === "https:" ||
|
|
106
|
+
parsed.origin === window.location.origin
|
|
107
|
+
? parsed.href
|
|
108
|
+
: null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function policyCopy(policy: CreativeContextPolicy) {
|
|
115
|
+
switch (policy) {
|
|
116
|
+
case "review":
|
|
117
|
+
return "New resources wait for reviewer approval before they are reused.";
|
|
118
|
+
case "admins-only":
|
|
119
|
+
return "Only administrators can approve or remove resources.";
|
|
120
|
+
default:
|
|
121
|
+
return "New resources are published after submission.";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatResourceTimestamp(value: string | undefined) {
|
|
126
|
+
if (!value) return null;
|
|
127
|
+
const date = new Date(value);
|
|
128
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
129
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
130
|
+
dateStyle: "medium",
|
|
131
|
+
timeStyle: "short",
|
|
132
|
+
}).format(date);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ResourcePreview({
|
|
136
|
+
resource,
|
|
137
|
+
}: {
|
|
138
|
+
resource: CreativeContextResourceDescriptor;
|
|
139
|
+
}) {
|
|
140
|
+
const imageUrl = creativeContextSafePreviewUrl(resource.preview?.imageUrl);
|
|
141
|
+
if (imageUrl) {
|
|
142
|
+
return (
|
|
143
|
+
<img
|
|
144
|
+
src={imageUrl}
|
|
145
|
+
alt={resource.preview?.alt ?? ""}
|
|
146
|
+
className="size-11 rounded-md border border-border object-cover"
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex size-11 items-center justify-center rounded-md border border-border bg-muted text-muted-foreground">
|
|
152
|
+
<IconFileText className="size-5" />
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function submitCreativeContextResources({
|
|
158
|
+
contextId,
|
|
159
|
+
resources,
|
|
160
|
+
rank,
|
|
161
|
+
purpose,
|
|
162
|
+
note,
|
|
163
|
+
confirmBroaderPublication,
|
|
164
|
+
mutateAsync,
|
|
165
|
+
}: {
|
|
166
|
+
contextId: string;
|
|
167
|
+
resources: readonly CreativeContextResourceDescriptor[];
|
|
168
|
+
rank: CreativeContextMembershipRank;
|
|
169
|
+
purpose?: string;
|
|
170
|
+
note?: string;
|
|
171
|
+
confirmBroaderPublication?: true;
|
|
172
|
+
mutateAsync: (input: {
|
|
173
|
+
operation: "submit";
|
|
174
|
+
contextId: string;
|
|
175
|
+
nativeResource: {
|
|
176
|
+
appId: string;
|
|
177
|
+
resourceType: string;
|
|
178
|
+
resourceId: string;
|
|
179
|
+
expectedUpdatedAt?: string;
|
|
180
|
+
};
|
|
181
|
+
rank: CreativeContextMembershipRank;
|
|
182
|
+
purpose?: string;
|
|
183
|
+
note?: string;
|
|
184
|
+
confirmBroaderPublication?: true;
|
|
185
|
+
}) => Promise<unknown>;
|
|
186
|
+
}) {
|
|
187
|
+
const results = await Promise.allSettled(
|
|
188
|
+
resources.map((resource) =>
|
|
189
|
+
mutateAsync({
|
|
190
|
+
operation: "submit",
|
|
191
|
+
contextId,
|
|
192
|
+
nativeResource: {
|
|
193
|
+
appId: resource.appId,
|
|
194
|
+
resourceType: resource.resourceType,
|
|
195
|
+
resourceId: resource.resourceId,
|
|
196
|
+
expectedUpdatedAt: resource.updatedAt,
|
|
197
|
+
},
|
|
198
|
+
rank,
|
|
199
|
+
purpose,
|
|
200
|
+
note,
|
|
201
|
+
confirmBroaderPublication,
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
submitted: results.filter((result) => result.status === "fulfilled").length,
|
|
207
|
+
failed: results.filter((result) => result.status === "rejected").length,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function MembershipRow({
|
|
212
|
+
membership,
|
|
213
|
+
updateAvailable,
|
|
214
|
+
canReview,
|
|
215
|
+
canWithdraw,
|
|
216
|
+
canRemove,
|
|
217
|
+
busy,
|
|
218
|
+
onAction,
|
|
219
|
+
}: {
|
|
220
|
+
membership: CreativeContextMembership;
|
|
221
|
+
updateAvailable: boolean;
|
|
222
|
+
canReview: boolean;
|
|
223
|
+
canWithdraw: boolean;
|
|
224
|
+
canRemove: boolean;
|
|
225
|
+
busy: boolean;
|
|
226
|
+
onAction: (
|
|
227
|
+
operation: "approve" | "request-changes" | "withdraw" | "remove",
|
|
228
|
+
) => void;
|
|
229
|
+
}) {
|
|
230
|
+
const pending = Boolean(membership.pendingSubmissionId);
|
|
231
|
+
return (
|
|
232
|
+
<article className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border p-3">
|
|
233
|
+
<div>
|
|
234
|
+
<p className="text-sm font-medium">
|
|
235
|
+
{pending ? "Pending resource" : "Published resource"}
|
|
236
|
+
</p>
|
|
237
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
238
|
+
{membership.rank}{" "}
|
|
239
|
+
{membership.purpose ? `· ${membership.purpose}` : ""}
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="flex flex-wrap gap-2">
|
|
243
|
+
{pending ? (
|
|
244
|
+
<Badge variant="outline">Pending review</Badge>
|
|
245
|
+
) : (
|
|
246
|
+
<Badge variant="secondary">Published</Badge>
|
|
247
|
+
)}
|
|
248
|
+
{updateAvailable ? (
|
|
249
|
+
<Badge variant="outline">Update available</Badge>
|
|
250
|
+
) : null}
|
|
251
|
+
{pending && canWithdraw ? (
|
|
252
|
+
<Button
|
|
253
|
+
type="button"
|
|
254
|
+
variant="outline"
|
|
255
|
+
size="sm"
|
|
256
|
+
disabled={busy}
|
|
257
|
+
onClick={() => onAction("withdraw")}
|
|
258
|
+
>
|
|
259
|
+
Withdraw
|
|
260
|
+
</Button>
|
|
261
|
+
) : null}
|
|
262
|
+
{pending && canReview ? (
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
size="sm"
|
|
266
|
+
disabled={busy}
|
|
267
|
+
onClick={() => onAction("approve")}
|
|
268
|
+
>
|
|
269
|
+
<IconCheck /> Approve
|
|
270
|
+
</Button>
|
|
271
|
+
) : null}
|
|
272
|
+
{pending && canReview ? (
|
|
273
|
+
<Button
|
|
274
|
+
type="button"
|
|
275
|
+
size="sm"
|
|
276
|
+
variant="outline"
|
|
277
|
+
disabled={busy}
|
|
278
|
+
onClick={() => onAction("request-changes")}
|
|
279
|
+
>
|
|
280
|
+
Request changes
|
|
281
|
+
</Button>
|
|
282
|
+
) : null}
|
|
283
|
+
{canRemove ? (
|
|
284
|
+
<Button
|
|
285
|
+
type="button"
|
|
286
|
+
size="sm"
|
|
287
|
+
variant="ghost"
|
|
288
|
+
disabled={busy}
|
|
289
|
+
onClick={() => onAction("remove")}
|
|
290
|
+
>
|
|
291
|
+
<IconX /> Remove
|
|
292
|
+
</Button>
|
|
293
|
+
) : null}
|
|
294
|
+
</div>
|
|
295
|
+
</article>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ContextSelect({
|
|
300
|
+
contexts,
|
|
301
|
+
contextId,
|
|
302
|
+
onValueChange,
|
|
303
|
+
}: {
|
|
304
|
+
contexts: CreativeContextSummary[];
|
|
305
|
+
contextId: string;
|
|
306
|
+
onValueChange: (value: string) => void;
|
|
307
|
+
}) {
|
|
308
|
+
return (
|
|
309
|
+
<Select value={contextId} onValueChange={onValueChange}>
|
|
310
|
+
<SelectTrigger>
|
|
311
|
+
<SelectValue placeholder="Choose a context" />
|
|
312
|
+
</SelectTrigger>
|
|
313
|
+
<SelectContent>
|
|
314
|
+
{contexts.map((context) => (
|
|
315
|
+
<SelectItem key={context.id} value={context.id}>
|
|
316
|
+
{context.name}
|
|
317
|
+
</SelectItem>
|
|
318
|
+
))}
|
|
319
|
+
</SelectContent>
|
|
320
|
+
</Select>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function CreativeContextShareTab({
|
|
325
|
+
resource,
|
|
326
|
+
resources,
|
|
327
|
+
className,
|
|
328
|
+
}: CreativeContextShareTabProps) {
|
|
329
|
+
const contextsQuery = useCreativeContexts();
|
|
330
|
+
const manageContext = useManageCreativeContext();
|
|
331
|
+
const manageMembership = useManageContextMembership();
|
|
332
|
+
const contexts = parseCreativeContexts(contextsQuery.data);
|
|
333
|
+
const selectedResources = normalizeCreativeContextResources(
|
|
334
|
+
resource,
|
|
335
|
+
resources,
|
|
336
|
+
);
|
|
337
|
+
const primaryResource = selectedResources[0];
|
|
338
|
+
const updatedAt = formatResourceTimestamp(primaryResource?.updatedAt);
|
|
339
|
+
const [contextId, setContextId] = useState("");
|
|
340
|
+
const membershipsQuery = useContextMemberships(
|
|
341
|
+
contextId ? { contextId } : null,
|
|
342
|
+
);
|
|
343
|
+
const memberships = parseContextMembershipsForResource(
|
|
344
|
+
membershipsQuery.data,
|
|
345
|
+
primaryResource ?? { appId: "", resourceType: "", resourceId: "" },
|
|
346
|
+
);
|
|
347
|
+
const [rank, setRank] = useState<CreativeContextMembershipRank>("normal");
|
|
348
|
+
const [purpose, setPurpose] = useState("");
|
|
349
|
+
const [note, setNote] = useState("");
|
|
350
|
+
const [newContextName, setNewContextName] = useState("");
|
|
351
|
+
const [error, setError] = useState<string | null>(null);
|
|
352
|
+
const [submitSummary, setSubmitSummary] = useState<string | null>(null);
|
|
353
|
+
const [confirmedBroaderPublication, setConfirmedBroaderPublication] =
|
|
354
|
+
useState(false);
|
|
355
|
+
const busy = manageContext.isPending || manageMembership.isPending;
|
|
356
|
+
const selectedContext = contexts.find((context) => context.id === contextId);
|
|
357
|
+
const canCreateContext = contexts.some((context) => context.access.canAdmin);
|
|
358
|
+
const needsBroaderPublicationConfirmation = selectedResources.some((item) =>
|
|
359
|
+
requiresBroaderPublication(item, selectedContext),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (!contextId && contexts[0]?.id) setContextId(contexts[0].id);
|
|
364
|
+
}, [contextId, contexts]);
|
|
365
|
+
|
|
366
|
+
async function refresh() {
|
|
367
|
+
await Promise.all([contextsQuery.refetch(), membershipsQuery.refetch()]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function submit() {
|
|
371
|
+
if (
|
|
372
|
+
!contextId ||
|
|
373
|
+
!selectedResources.length ||
|
|
374
|
+
(needsBroaderPublicationConfirmation && !confirmedBroaderPublication)
|
|
375
|
+
)
|
|
376
|
+
return;
|
|
377
|
+
setError(null);
|
|
378
|
+
try {
|
|
379
|
+
const result = await submitCreativeContextResources({
|
|
380
|
+
contextId,
|
|
381
|
+
resources: selectedResources,
|
|
382
|
+
rank,
|
|
383
|
+
purpose: purpose.trim() || undefined,
|
|
384
|
+
note: note.trim() || undefined,
|
|
385
|
+
confirmBroaderPublication: needsBroaderPublicationConfirmation
|
|
386
|
+
? true
|
|
387
|
+
: undefined,
|
|
388
|
+
mutateAsync: manageMembership.mutateAsync,
|
|
389
|
+
});
|
|
390
|
+
setPurpose("");
|
|
391
|
+
setNote("");
|
|
392
|
+
setConfirmedBroaderPublication(false);
|
|
393
|
+
setSubmitSummary(
|
|
394
|
+
result.failed
|
|
395
|
+
? `${result.submitted} submitted; ${result.failed} could not be submitted.`
|
|
396
|
+
: `${result.submitted} ${result.submitted === 1 ? "resource" : "resources"} submitted.`,
|
|
397
|
+
);
|
|
398
|
+
await refresh();
|
|
399
|
+
} catch {
|
|
400
|
+
setError("Could not submit this resource to the selected context.");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function createContext() {
|
|
405
|
+
if (!newContextName.trim()) return;
|
|
406
|
+
setError(null);
|
|
407
|
+
try {
|
|
408
|
+
const result = await manageContext.mutateAsync({
|
|
409
|
+
operation: "create",
|
|
410
|
+
name: newContextName.trim(),
|
|
411
|
+
kind: "specialty",
|
|
412
|
+
approvalPolicy: "open",
|
|
413
|
+
});
|
|
414
|
+
setNewContextName("");
|
|
415
|
+
await contextsQuery.refetch();
|
|
416
|
+
if (result.context?.id) setContextId(result.context.id);
|
|
417
|
+
} catch {
|
|
418
|
+
setError("Could not create a context.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function act(
|
|
423
|
+
membershipId: string,
|
|
424
|
+
operation: "approve" | "request-changes" | "withdraw" | "remove",
|
|
425
|
+
) {
|
|
426
|
+
if (!contextId) return;
|
|
427
|
+
setError(null);
|
|
428
|
+
try {
|
|
429
|
+
await manageMembership.mutateAsync({
|
|
430
|
+
operation,
|
|
431
|
+
contextId,
|
|
432
|
+
membershipId,
|
|
433
|
+
});
|
|
434
|
+
await refresh();
|
|
435
|
+
} catch {
|
|
436
|
+
setError("Could not update this context membership.");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<section className={className} aria-label="Creative context">
|
|
442
|
+
<div className="flex items-start gap-3 border-b border-border/70 pb-4">
|
|
443
|
+
{primaryResource ? (
|
|
444
|
+
<ResourcePreview resource={primaryResource} />
|
|
445
|
+
) : null}
|
|
446
|
+
<div className="min-w-0">
|
|
447
|
+
<p className="truncate text-sm font-medium">
|
|
448
|
+
{selectedResources.length === 1
|
|
449
|
+
? primaryResource?.title
|
|
450
|
+
: `${selectedResources.length} selected resources`}
|
|
451
|
+
</p>
|
|
452
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
453
|
+
{selectedResources.length === 1
|
|
454
|
+
? (primaryResource?.preview?.label ??
|
|
455
|
+
primaryResource?.resourceType)
|
|
456
|
+
: "Each resource is submitted separately"}
|
|
457
|
+
</p>
|
|
458
|
+
{selectedResources.length === 1 && updatedAt ? (
|
|
459
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
460
|
+
Current version · {updatedAt}
|
|
461
|
+
</p>
|
|
462
|
+
) : null}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
<Tabs defaultValue="contexts" className="mt-4">
|
|
466
|
+
<TabsList className="w-full justify-start">
|
|
467
|
+
<TabsTrigger value="contexts">Contexts</TabsTrigger>
|
|
468
|
+
<TabsTrigger value="policy">Policy</TabsTrigger>
|
|
469
|
+
</TabsList>
|
|
470
|
+
<TabsContent value="contexts" className="space-y-3">
|
|
471
|
+
<ContextSelect
|
|
472
|
+
contexts={contexts}
|
|
473
|
+
contextId={contextId}
|
|
474
|
+
onValueChange={setContextId}
|
|
475
|
+
/>
|
|
476
|
+
{selectedContext ? (
|
|
477
|
+
<p className="text-xs text-muted-foreground">
|
|
478
|
+
{selectedContext.description ||
|
|
479
|
+
`${selectedContext.memberCount} published resources`}{" "}
|
|
480
|
+
· {policyCopy(selectedContext.approvalPolicy)}
|
|
481
|
+
</p>
|
|
482
|
+
) : (
|
|
483
|
+
<p className="text-sm text-muted-foreground">
|
|
484
|
+
No contexts are available yet.
|
|
485
|
+
</p>
|
|
486
|
+
)}
|
|
487
|
+
{contextId && selectedResources.length === 1 ? (
|
|
488
|
+
<div className="space-y-2">
|
|
489
|
+
{memberships.map((membership) => (
|
|
490
|
+
<MembershipRow
|
|
491
|
+
key={membership.id}
|
|
492
|
+
membership={membership}
|
|
493
|
+
updateAvailable={Boolean(
|
|
494
|
+
primaryResource?.updatedAt &&
|
|
495
|
+
membership.publishedItem?.sourceModifiedAt &&
|
|
496
|
+
primaryResource.updatedAt !==
|
|
497
|
+
membership.publishedItem.sourceModifiedAt,
|
|
498
|
+
)}
|
|
499
|
+
canReview={selectedContext?.access.canReview === true}
|
|
500
|
+
canWithdraw={
|
|
501
|
+
selectedContext?.access.canReview === true ||
|
|
502
|
+
selectedContext?.access.canSubmit === true
|
|
503
|
+
}
|
|
504
|
+
canRemove={selectedContext?.access.canAdmin === true}
|
|
505
|
+
busy={busy}
|
|
506
|
+
onAction={(operation) => void act(membership.id, operation)}
|
|
507
|
+
/>
|
|
508
|
+
))}
|
|
509
|
+
{!memberships.length && !membershipsQuery.isLoading ? (
|
|
510
|
+
<p className="text-sm text-muted-foreground">
|
|
511
|
+
This resource has not been submitted to this context.
|
|
512
|
+
</p>
|
|
513
|
+
) : null}
|
|
514
|
+
</div>
|
|
515
|
+
) : null}
|
|
516
|
+
{contextId && selectedResources.length ? (
|
|
517
|
+
<div className="rounded-md border border-dashed border-border p-3">
|
|
518
|
+
<p className="text-sm font-medium">
|
|
519
|
+
{memberships.some((membership) => membership.publishedItem)
|
|
520
|
+
? "Submit update for "
|
|
521
|
+
: "Add "}
|
|
522
|
+
{selectedResources.length === 1
|
|
523
|
+
? "this resource"
|
|
524
|
+
: `${selectedResources.length} resources`}
|
|
525
|
+
</p>
|
|
526
|
+
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
|
527
|
+
<Select
|
|
528
|
+
value={rank}
|
|
529
|
+
onValueChange={(value) =>
|
|
530
|
+
setRank(value as CreativeContextMembershipRank)
|
|
531
|
+
}
|
|
532
|
+
>
|
|
533
|
+
<SelectTrigger>
|
|
534
|
+
<SelectValue />
|
|
535
|
+
</SelectTrigger>
|
|
536
|
+
<SelectContent>
|
|
537
|
+
<SelectItem value="canonical">Canonical</SelectItem>
|
|
538
|
+
<SelectItem value="exemplar">Exemplar</SelectItem>
|
|
539
|
+
<SelectItem value="normal">Reference</SelectItem>
|
|
540
|
+
</SelectContent>
|
|
541
|
+
</Select>
|
|
542
|
+
<Input
|
|
543
|
+
value={purpose}
|
|
544
|
+
onChange={(event) => setPurpose(event.target.value)}
|
|
545
|
+
placeholder="Purpose"
|
|
546
|
+
/>
|
|
547
|
+
</div>
|
|
548
|
+
<Textarea
|
|
549
|
+
className="mt-2"
|
|
550
|
+
value={note}
|
|
551
|
+
onChange={(event) => setNote(event.target.value)}
|
|
552
|
+
placeholder="Note for reviewers"
|
|
553
|
+
rows={2}
|
|
554
|
+
/>
|
|
555
|
+
{needsBroaderPublicationConfirmation ? (
|
|
556
|
+
<label className="mt-3 flex items-start gap-2 text-xs text-muted-foreground">
|
|
557
|
+
<Checkbox
|
|
558
|
+
checked={confirmedBroaderPublication}
|
|
559
|
+
onCheckedChange={(checked) =>
|
|
560
|
+
setConfirmedBroaderPublication(checked === true)
|
|
561
|
+
}
|
|
562
|
+
/>
|
|
563
|
+
<span>
|
|
564
|
+
This context is shared more broadly than this resource.
|
|
565
|
+
Publishing creates a governed copy available to the
|
|
566
|
+
context's audience.
|
|
567
|
+
</span>
|
|
568
|
+
</label>
|
|
569
|
+
) : null}
|
|
570
|
+
<Button
|
|
571
|
+
type="button"
|
|
572
|
+
className="mt-3"
|
|
573
|
+
size="sm"
|
|
574
|
+
disabled={
|
|
575
|
+
busy ||
|
|
576
|
+
selectedContext?.access.canSubmit !== true ||
|
|
577
|
+
(needsBroaderPublicationConfirmation &&
|
|
578
|
+
!confirmedBroaderPublication)
|
|
579
|
+
}
|
|
580
|
+
onClick={() => void submit()}
|
|
581
|
+
>
|
|
582
|
+
<IconLink /> Submit
|
|
583
|
+
</Button>
|
|
584
|
+
</div>
|
|
585
|
+
) : null}
|
|
586
|
+
{canCreateContext ? (
|
|
587
|
+
<div className="flex gap-2 border-t border-border/60 pt-3">
|
|
588
|
+
<Input
|
|
589
|
+
value={newContextName}
|
|
590
|
+
onChange={(event) => setNewContextName(event.target.value)}
|
|
591
|
+
placeholder="New context name"
|
|
592
|
+
/>
|
|
593
|
+
<Button
|
|
594
|
+
type="button"
|
|
595
|
+
variant="outline"
|
|
596
|
+
size="sm"
|
|
597
|
+
disabled={busy || !newContextName.trim()}
|
|
598
|
+
onClick={() => void createContext()}
|
|
599
|
+
>
|
|
600
|
+
<IconPlus /> New
|
|
601
|
+
</Button>
|
|
602
|
+
</div>
|
|
603
|
+
) : null}
|
|
604
|
+
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
605
|
+
{submitSummary ? (
|
|
606
|
+
<p className="text-xs text-muted-foreground">{submitSummary}</p>
|
|
607
|
+
) : null}
|
|
608
|
+
</TabsContent>
|
|
609
|
+
<TabsContent
|
|
610
|
+
value="policy"
|
|
611
|
+
className="space-y-3 text-sm text-muted-foreground"
|
|
612
|
+
>
|
|
613
|
+
<div className="flex gap-2 rounded-md border border-border p-3">
|
|
614
|
+
<IconShieldCheck className="mt-0.5 size-4 shrink-0" />
|
|
615
|
+
<p>
|
|
616
|
+
Open contexts publish submitted resources, review contexts wait
|
|
617
|
+
for approval, and admins-only contexts require an administrator.
|
|
618
|
+
</p>
|
|
619
|
+
</div>
|
|
620
|
+
{selectedContext ? (
|
|
621
|
+
<p>{policyCopy(selectedContext.approvalPolicy)}</p>
|
|
622
|
+
) : null}
|
|
623
|
+
</TabsContent>
|
|
624
|
+
</Tabs>
|
|
625
|
+
</section>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function CreativeContextShareSheet({
|
|
630
|
+
resource,
|
|
631
|
+
resources,
|
|
632
|
+
open,
|
|
633
|
+
onOpenChange,
|
|
634
|
+
canManage,
|
|
635
|
+
}: CreativeContextShareTabProps & {
|
|
636
|
+
open: boolean;
|
|
637
|
+
onOpenChange: (open: boolean) => void;
|
|
638
|
+
}) {
|
|
639
|
+
return (
|
|
640
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
641
|
+
<SheetContent side="right" className="w-full overflow-y-auto sm:max-w-lg">
|
|
642
|
+
<SheetHeader>
|
|
643
|
+
<SheetTitle>Creative context</SheetTitle>
|
|
644
|
+
<SheetDescription>
|
|
645
|
+
Place this resource in a governed context for future reuse.
|
|
646
|
+
</SheetDescription>
|
|
647
|
+
</SheetHeader>
|
|
648
|
+
<CreativeContextShareTab
|
|
649
|
+
resource={resource}
|
|
650
|
+
resources={resources}
|
|
651
|
+
canManage={canManage}
|
|
652
|
+
className="mt-5"
|
|
653
|
+
/>
|
|
654
|
+
</SheetContent>
|
|
655
|
+
</Sheet>
|
|
656
|
+
);
|
|
657
|
+
}
|