@tangle-network/agent-app 0.35.0 → 0.37.1

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.
@@ -0,0 +1,293 @@
1
+ // src/studio/generation.ts
2
+ var GENERATION_TYPES = ["image", "video", "avatar", "speech", "transcription"];
3
+ function isGenerationType(value) {
4
+ return GENERATION_TYPES.includes(value);
5
+ }
6
+ var DESTINATIONS = [
7
+ { id: "instagram", label: "Instagram", providerIds: ["instagram"], fields: "Caption, hashtags, crop" },
8
+ { id: "tiktok", label: "TikTok", providerIds: ["tiktok"], fields: "Caption, audio note, vertical video" },
9
+ { id: "youtube-shorts", label: "YouTube Shorts", providerIds: ["youtube"], fields: "Title, description, vertical video" },
10
+ { id: "linkedin", label: "LinkedIn", providerIds: ["linkedin"], fields: "Caption, link, creator/company page" },
11
+ { id: "x", label: "X", providerIds: ["twitter"], fields: "Short copy, mentions, thread option" }
12
+ ];
13
+ var CADENCES = ["Manual approval", "Publish now", "Daily creative drop", "Weekly series"];
14
+ var MIN_IMAGE_COUNT = 1;
15
+ var MAX_IMAGE_COUNT = 4;
16
+ function relativeTime(date) {
17
+ if (!date) return "";
18
+ const now = Date.now();
19
+ const diff = now - new Date(date).getTime();
20
+ const minutes = Math.floor(diff / 6e4);
21
+ if (minutes < 1) return "just now";
22
+ if (minutes < 60) return `${minutes}m ago`;
23
+ const hours = Math.floor(minutes / 60);
24
+ if (hours < 24) return `${hours}h ago`;
25
+ const days = Math.floor(hours / 24);
26
+ if (days < 7) return `${days}d ago`;
27
+ return new Date(date).toLocaleDateString("en-US", { month: "short", day: "numeric" });
28
+ }
29
+ function outputPathFor(type) {
30
+ if (type === "image") return "generated/images";
31
+ if (type === "video") return "generated/videos";
32
+ if (type === "avatar") return "generated/avatars";
33
+ if (type === "speech") return "generated/audio";
34
+ return "generated/transcripts";
35
+ }
36
+ function generationVaultPath(generation) {
37
+ const value = generation.metadata?.vaultPath;
38
+ return typeof value === "string" && value.trim() ? value.trim() : null;
39
+ }
40
+ function buildPublishPackage({
41
+ caption,
42
+ postDescription,
43
+ mentions,
44
+ cadence,
45
+ destinations
46
+ }) {
47
+ const trimmedCaption = caption.trim();
48
+ const trimmedDescription = postDescription.trim();
49
+ const parsedMentions = mentions.split(",").map((item) => item.trim()).filter(Boolean);
50
+ const selectedDestinations = destinations.filter(Boolean);
51
+ const trimmedCadence = cadence.trim() || "Manual approval";
52
+ const hasPublishContent = Boolean(
53
+ trimmedCaption || trimmedDescription || parsedMentions.length > 0 || selectedDestinations.length > 0 || trimmedCadence !== "Manual approval"
54
+ );
55
+ if (!hasPublishContent) return null;
56
+ return {
57
+ caption: trimmedCaption,
58
+ description: trimmedDescription,
59
+ mentions: parsedMentions,
60
+ destinations: selectedDestinations,
61
+ cadence: trimmedCadence,
62
+ workflowDraft: trimmedCadence !== "Manual approval",
63
+ evalContract: {
64
+ artifactType: "publish_package",
65
+ deterministicChecks: ["has_asset", "has_destination", "has_caption_or_description", "has_cadence"]
66
+ }
67
+ };
68
+ }
69
+ function isPublishPackage(value) {
70
+ if (!value || typeof value !== "object") return false;
71
+ const publishPackage = value;
72
+ const destinations = Array.isArray(publishPackage.destinations) ? publishPackage.destinations.filter(Boolean) : [];
73
+ const mentions = Array.isArray(publishPackage.mentions) ? publishPackage.mentions.filter(Boolean) : [];
74
+ return Boolean(
75
+ destinations.length > 0 || mentions.length > 0 || typeof publishPackage.caption === "string" && publishPackage.caption.trim() || typeof publishPackage.description === "string" && publishPackage.description.trim() || typeof publishPackage.cadence === "string" && publishPackage.cadence.trim() && publishPackage.cadence.trim() !== "Manual approval"
76
+ );
77
+ }
78
+ function isDestinationConnected(destination, connections) {
79
+ return connections.some((connection) => connection.status === "connected" && (destination.providerIds.includes(connection.providerId) || destination.providerIds.includes(connection.connectorId)));
80
+ }
81
+ function selectedModelsWithDefaults(current, catalog) {
82
+ const next = { ...current };
83
+ for (const key of GENERATION_TYPES) {
84
+ const models = catalog.models[key] ?? [];
85
+ const currentOption = models.find((model) => model.id === next[key]);
86
+ if (!next[key] || !currentOption || currentOption.status === "unavailable") {
87
+ next[key] = preferredModelId(key, catalog) ?? "";
88
+ }
89
+ }
90
+ return next;
91
+ }
92
+ function preferredModelId(type, catalog) {
93
+ if (!catalog) return void 0;
94
+ const models = catalog.models[type] ?? [];
95
+ const preferred = catalog.defaults[type];
96
+ return models.find((model) => model.id === preferred)?.id ?? models.find((model) => model.status !== "unavailable")?.id ?? models[0]?.id;
97
+ }
98
+ function modelMessage(model, loading, count) {
99
+ if (loading) return "Loading media models...";
100
+ if (count === 0) return "No models are available for this media type.";
101
+ if (!model) return "Select a model.";
102
+ if (model.status === "unavailable") return model.reason ?? "This model is not configured.";
103
+ if (model.status === "limited") return model.reason ? `Limited: ${model.reason}` : "Limited availability.";
104
+ return null;
105
+ }
106
+ function buildGenerationRequestBody(fields) {
107
+ const body = {
108
+ workspaceId: fields.workspaceId,
109
+ clientRequestId: fields.clientRequestId,
110
+ type: fields.type,
111
+ model: fields.model,
112
+ prompt: fields.prompt.trim(),
113
+ negativePrompt: fields.negativePrompt.trim() || void 0,
114
+ outputPath: fields.outputPath.trim() || void 0
115
+ };
116
+ if (fields.publishPackage) body.publishPackage = fields.publishPackage;
117
+ if (fields.type === "image") Object.assign(body, {
118
+ size: fields.image.size,
119
+ quality: fields.image.quality,
120
+ n: fields.image.count
121
+ });
122
+ if (fields.type === "video") {
123
+ const duration = Number(fields.video.duration);
124
+ Object.assign(body, {
125
+ // omit (let the API default) rather than serialize NaN → null on bad input
126
+ duration: Number.isFinite(duration) ? duration : void 0,
127
+ resolution: fields.video.resolution,
128
+ aspectRatio: fields.video.aspectRatio.trim() || void 0,
129
+ referenceImageUrl: fields.video.referenceImageUrl.trim() || void 0
130
+ });
131
+ }
132
+ if (fields.type === "speech") Object.assign(body, { voice: fields.speech.voice });
133
+ if (fields.type === "avatar") Object.assign(body, {
134
+ audioUrl: fields.avatar.audioUrl.trim(),
135
+ imageUrl: fields.avatar.imageUrl.trim() || void 0,
136
+ avatarId: fields.avatar.avatarId.trim() || void 0
137
+ });
138
+ if (fields.type === "transcription") {
139
+ const temperature = Number(fields.transcription.temperature);
140
+ Object.assign(body, {
141
+ audioUrl: fields.transcription.audioUrl.trim(),
142
+ language: fields.transcription.language.trim() || void 0,
143
+ responseFormat: fields.transcription.responseFormat,
144
+ // omit (let the API default) rather than serialize NaN → null on bad input
145
+ temperature: Number.isFinite(temperature) ? temperature : void 0
146
+ });
147
+ }
148
+ return body;
149
+ }
150
+ function generationStatus(generation) {
151
+ const metadata = generation.metadata ?? {};
152
+ const status = typeof metadata.generationStatus === "string" ? metadata.generationStatus : "";
153
+ if (status === "pending" || status === "running" || status === "failed" || status === "succeeded") return status;
154
+ return generation.result ? "succeeded" : "pending";
155
+ }
156
+ function generationError(generation) {
157
+ const metadata = generation.metadata ?? {};
158
+ if (typeof metadata.providerError === "string" && metadata.providerError.trim()) {
159
+ return userSafeGenerationMessage(metadata.providerError);
160
+ }
161
+ if (typeof metadata.storageError === "string" && metadata.storageError.trim()) {
162
+ return metadata.storageError;
163
+ }
164
+ return null;
165
+ }
166
+ function generationClientRequestId(generation) {
167
+ const metadata = generation.metadata ?? {};
168
+ return typeof metadata.clientRequestId === "string" && metadata.clientRequestId.trim() ? metadata.clientRequestId : null;
169
+ }
170
+ function generationBatchSlotKey(generation) {
171
+ const metadata = generation.metadata ?? {};
172
+ const batchId = typeof metadata.batchId === "string" && metadata.batchId.trim() ? metadata.batchId : null;
173
+ return batchId && typeof metadata.outputIndex === "number" ? `${batchId}:${metadata.outputIndex}` : null;
174
+ }
175
+ function generationMergeKey(generation) {
176
+ return generationBatchSlotKey(generation) ?? generationClientRequestId(generation);
177
+ }
178
+ function mergeLiveGeneration(current, generation) {
179
+ const mergeKey = generationMergeKey(generation);
180
+ const existingIndex = current.findIndex((item) => item.id === generation.id || mergeKey && generationMergeKey(item) === mergeKey);
181
+ if (existingIndex === -1) return [generation, ...current];
182
+ const next = [...current];
183
+ next[existingIndex] = generation;
184
+ return next;
185
+ }
186
+ function mergeLoaderAndLive(loader, live) {
187
+ if (live.length === 0) return loader;
188
+ const leading = live.map((generation) => {
189
+ const mergeKey = generationMergeKey(generation);
190
+ return mergeKey ? loader.find((gen) => generationMergeKey(gen) === mergeKey) ?? generation : loader.find((gen) => gen.id === generation.id) ?? generation;
191
+ });
192
+ const leadingIds = new Set(leading.map((gen) => gen.id));
193
+ const leadingMergeKeys = new Set(leading.map((gen) => generationMergeKey(gen)).filter((id) => Boolean(id)));
194
+ return [
195
+ ...leading,
196
+ ...loader.filter((gen) => !leadingIds.has(gen.id) && !leadingMergeKeys.has(generationMergeKey(gen) ?? ""))
197
+ ];
198
+ }
199
+ function isLocalGeneration(generation) {
200
+ return generation.id.startsWith("local-");
201
+ }
202
+ function generationOutputIndex(generation) {
203
+ const value = generation.metadata?.outputIndex;
204
+ return typeof value === "number" ? value : 0;
205
+ }
206
+ function latestBatchOf(generations) {
207
+ const first = generations[0];
208
+ if (!first) return [];
209
+ const key = generationClientRequestId(first);
210
+ const batch = key ? generations.filter((generation) => generationClientRequestId(generation) === key) : [first];
211
+ return [...batch].sort((a, b) => generationOutputIndex(a) - generationOutputIndex(b));
212
+ }
213
+ function userSafeGenerationMessage(message) {
214
+ if (!message) return "Generation failed";
215
+ if (/Tangle API key is invalid or expired/i.test(message)) return message;
216
+ if (/(api[_ -]?key|secret|token|credential|env|configured|configuration)/i.test(message)) {
217
+ return "Generation failed";
218
+ }
219
+ return message;
220
+ }
221
+ function optimisticGeneration({
222
+ type,
223
+ prompt,
224
+ model,
225
+ clientRequestId,
226
+ outputIndex,
227
+ outputCount
228
+ }) {
229
+ const batchId = outputIndex == null ? void 0 : clientRequestId;
230
+ return {
231
+ id: outputIndex == null ? `local-${clientRequestId}` : `local-${clientRequestId}-${outputIndex}`,
232
+ type,
233
+ prompt,
234
+ result: null,
235
+ model: model ?? null,
236
+ cost: null,
237
+ createdAt: /* @__PURE__ */ new Date(),
238
+ metadata: {
239
+ generationStatus: "pending",
240
+ provider: type,
241
+ clientRequestId,
242
+ batchId,
243
+ outputIndex,
244
+ outputCount
245
+ }
246
+ };
247
+ }
248
+ function failedOptimisticGeneration(generation) {
249
+ return {
250
+ ...generation,
251
+ metadata: {
252
+ ...generation.metadata ?? {},
253
+ generationStatus: "failed",
254
+ providerError: "Generation failed"
255
+ }
256
+ };
257
+ }
258
+ function normalizeImageCount(value) {
259
+ const numeric = typeof value === "number" ? value : Number(value);
260
+ if (!Number.isFinite(numeric)) return MIN_IMAGE_COUNT;
261
+ return Math.min(Math.max(Math.trunc(numeric), MIN_IMAGE_COUNT), MAX_IMAGE_COUNT);
262
+ }
263
+
264
+ export {
265
+ GENERATION_TYPES,
266
+ isGenerationType,
267
+ DESTINATIONS,
268
+ CADENCES,
269
+ MIN_IMAGE_COUNT,
270
+ MAX_IMAGE_COUNT,
271
+ relativeTime,
272
+ outputPathFor,
273
+ generationVaultPath,
274
+ buildPublishPackage,
275
+ isPublishPackage,
276
+ isDestinationConnected,
277
+ selectedModelsWithDefaults,
278
+ preferredModelId,
279
+ modelMessage,
280
+ buildGenerationRequestBody,
281
+ generationStatus,
282
+ generationError,
283
+ generationMergeKey,
284
+ mergeLiveGeneration,
285
+ mergeLoaderAndLive,
286
+ isLocalGeneration,
287
+ latestBatchOf,
288
+ userSafeGenerationMessage,
289
+ optimisticGeneration,
290
+ failedOptimisticGeneration,
291
+ normalizeImageCount
292
+ };
293
+ //# sourceMappingURL=chunk-DTS5TZRN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/studio/generation.ts"],"sourcesContent":["// Minimal structural view of a connected integration — `isDestinationConnected`\n// only reads the connection status and the provider/connector identifiers, so\n// the logic layer declares its own shape instead of depending on the\n// design-system package. A sandbox-ui `IntegrationConnection` is assignable to\n// this (it carries all three fields).\nexport interface StudioIntegrationConnection {\n status: string\n providerId: string\n connectorId: string\n}\n\nexport type GenerationType = 'image' | 'video' | 'speech' | 'avatar' | 'transcription'\n\nexport type GenerationStatus = 'pending' | 'running' | 'succeeded' | 'failed'\n\nexport type MediaModelStatus = 'available' | 'limited' | 'unavailable'\n\nexport interface Generation {\n id: string\n type: string\n prompt: string\n result: string | null\n model: string | null\n cost: number | null\n createdAt: Date | null\n metadata: Record<string, unknown> | null\n}\n\nexport interface MediaModelOption {\n id: string\n name: string\n provider?: string\n type: GenerationType\n status: MediaModelStatus\n reason?: string\n}\n\nexport interface MediaModelCatalogResponse {\n defaults: Record<GenerationType, string>\n models: Record<GenerationType, MediaModelOption[]>\n error?: string\n}\n\nexport interface PublishPackage {\n caption: string\n description: string\n mentions: string[]\n destinations: string[]\n cadence: string\n workflowDraft: boolean\n evalContract: {\n artifactType: string\n deterministicChecks: string[]\n }\n}\n\nexport interface PublishDestination {\n id: string\n label: string\n providerIds: string[]\n fields: string\n}\n\n// Order drives the type filter tabs and the composer segmented control\nexport const GENERATION_TYPES: readonly GenerationType[] = ['image', 'video', 'avatar', 'speech', 'transcription']\n\nexport function isGenerationType(value: string): value is GenerationType {\n return (GENERATION_TYPES as readonly string[]).includes(value)\n}\n\nexport const DESTINATIONS: PublishDestination[] = [\n { id: 'instagram', label: 'Instagram', providerIds: ['instagram'], fields: 'Caption, hashtags, crop' },\n { id: 'tiktok', label: 'TikTok', providerIds: ['tiktok'], fields: 'Caption, audio note, vertical video' },\n { id: 'youtube-shorts', label: 'YouTube Shorts', providerIds: ['youtube'], fields: 'Title, description, vertical video' },\n { id: 'linkedin', label: 'LinkedIn', providerIds: ['linkedin'], fields: 'Caption, link, creator/company page' },\n { id: 'x', label: 'X', providerIds: ['twitter'], fields: 'Short copy, mentions, thread option' },\n]\n\nexport const CADENCES = ['Manual approval', 'Publish now', 'Daily creative drop', 'Weekly series']\n\nexport const MIN_IMAGE_COUNT = 1\nexport const MAX_IMAGE_COUNT = 4\n\nexport function relativeTime(date: Date | null): string {\n if (!date) return ''\n const now = Date.now()\n const diff = now - new Date(date).getTime()\n const minutes = Math.floor(diff / 60000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n const hours = Math.floor(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n const days = Math.floor(hours / 24)\n if (days < 7) return `${days}d ago`\n return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n}\n\nexport function outputPathFor(type: GenerationType): string {\n if (type === 'image') return 'generated/images'\n if (type === 'video') return 'generated/videos'\n if (type === 'avatar') return 'generated/avatars'\n if (type === 'speech') return 'generated/audio'\n return 'generated/transcripts'\n}\n\nexport function generationVaultPath(generation: Generation): string | null {\n const value = generation.metadata?.vaultPath\n return typeof value === 'string' && value.trim() ? value.trim() : null\n}\n\nexport function buildPublishPackage({\n caption,\n postDescription,\n mentions,\n cadence,\n destinations,\n}: {\n caption: string\n postDescription: string\n mentions: string\n cadence: string\n destinations: string[]\n}): PublishPackage | null {\n const trimmedCaption = caption.trim()\n const trimmedDescription = postDescription.trim()\n const parsedMentions = mentions.split(',').map((item) => item.trim()).filter(Boolean)\n const selectedDestinations = destinations.filter(Boolean)\n const trimmedCadence = cadence.trim() || 'Manual approval'\n const hasPublishContent = Boolean(\n trimmedCaption\n || trimmedDescription\n || parsedMentions.length > 0\n || selectedDestinations.length > 0\n || trimmedCadence !== 'Manual approval',\n )\n if (!hasPublishContent) return null\n\n return {\n caption: trimmedCaption,\n description: trimmedDescription,\n mentions: parsedMentions,\n destinations: selectedDestinations,\n cadence: trimmedCadence,\n workflowDraft: trimmedCadence !== 'Manual approval',\n evalContract: {\n artifactType: 'publish_package',\n deterministicChecks: ['has_asset', 'has_destination', 'has_caption_or_description', 'has_cadence'],\n },\n }\n}\n\nexport function isPublishPackage(value: unknown): value is { caption?: string; description?: string; mentions?: string[]; destinations?: string[]; cadence?: string } {\n if (!value || typeof value !== 'object') return false\n const publishPackage = value as {\n caption?: unknown\n description?: unknown\n mentions?: unknown\n destinations?: unknown\n cadence?: unknown\n }\n const destinations = Array.isArray(publishPackage.destinations) ? publishPackage.destinations.filter(Boolean) : []\n const mentions = Array.isArray(publishPackage.mentions) ? publishPackage.mentions.filter(Boolean) : []\n return Boolean(\n destinations.length > 0\n || mentions.length > 0\n || (typeof publishPackage.caption === 'string' && publishPackage.caption.trim())\n || (typeof publishPackage.description === 'string' && publishPackage.description.trim())\n || (typeof publishPackage.cadence === 'string' && publishPackage.cadence.trim() && publishPackage.cadence.trim() !== 'Manual approval'),\n )\n}\n\nexport function isDestinationConnected(\n destination: PublishDestination,\n connections: StudioIntegrationConnection[],\n): boolean {\n return connections.some((connection) => connection.status === 'connected'\n && (destination.providerIds.includes(connection.providerId) || destination.providerIds.includes(connection.connectorId)))\n}\n\nexport function selectedModelsWithDefaults(\n current: Partial<Record<GenerationType, string>>,\n catalog: MediaModelCatalogResponse,\n): Partial<Record<GenerationType, string>> {\n const next = { ...current }\n for (const key of GENERATION_TYPES) {\n const models = catalog.models[key] ?? []\n const currentOption = models.find((model) => model.id === next[key])\n // Reset when: no selection, selection not in catalog, or selection is unavailable.\n // This ensures the Generate button is never stuck disabled when routeable\n // models exist but the stored default isn't one of them.\n if (!next[key] || !currentOption || currentOption.status === 'unavailable') {\n next[key] = preferredModelId(key, catalog) ?? ''\n }\n }\n return next\n}\n\nexport function preferredModelId(type: GenerationType, catalog: MediaModelCatalogResponse | null): string | undefined {\n if (!catalog) return undefined\n const models = catalog.models[type] ?? []\n const preferred = catalog.defaults[type]\n return models.find((model) => model.id === preferred)?.id\n ?? models.find((model) => model.status !== 'unavailable')?.id\n ?? models[0]?.id\n}\n\nexport function modelMessage(model: MediaModelOption | undefined, loading: boolean, count: number): string | null {\n if (loading) return 'Loading media models...'\n if (count === 0) return 'No models are available for this media type.'\n if (!model) return 'Select a model.'\n if (model.status === 'unavailable') return model.reason ?? 'This model is not configured.'\n if (model.status === 'limited') return model.reason ? `Limited: ${model.reason}` : 'Limited availability.'\n return null\n}\n\nexport interface GenerationRequestFields {\n workspaceId: string\n clientRequestId: string\n type: GenerationType\n model: string\n prompt: string\n negativePrompt: string\n outputPath: string\n publishPackage: PublishPackage | null\n image: { size: string; quality: string; count: number }\n video: { duration: string; resolution: string; aspectRatio: string; referenceImageUrl: string }\n speech: { voice: string }\n avatar: { audioUrl: string; imageUrl: string; avatarId: string }\n transcription: { audioUrl: string; language: string; responseFormat: string; temperature: string }\n}\n\n// image.count must already be normalized — it is also the optimistic-card count on the caller side\nexport function buildGenerationRequestBody(fields: GenerationRequestFields): Record<string, unknown> {\n const body: Record<string, unknown> = {\n workspaceId: fields.workspaceId,\n clientRequestId: fields.clientRequestId,\n type: fields.type,\n model: fields.model,\n prompt: fields.prompt.trim(),\n negativePrompt: fields.negativePrompt.trim() || undefined,\n outputPath: fields.outputPath.trim() || undefined,\n }\n if (fields.publishPackage) body.publishPackage = fields.publishPackage\n if (fields.type === 'image') Object.assign(body, {\n size: fields.image.size,\n quality: fields.image.quality,\n n: fields.image.count,\n })\n if (fields.type === 'video') {\n const duration = Number(fields.video.duration)\n Object.assign(body, {\n // omit (let the API default) rather than serialize NaN → null on bad input\n duration: Number.isFinite(duration) ? duration : undefined,\n resolution: fields.video.resolution,\n aspectRatio: fields.video.aspectRatio.trim() || undefined,\n referenceImageUrl: fields.video.referenceImageUrl.trim() || undefined,\n })\n }\n if (fields.type === 'speech') Object.assign(body, { voice: fields.speech.voice })\n if (fields.type === 'avatar') Object.assign(body, {\n audioUrl: fields.avatar.audioUrl.trim(),\n imageUrl: fields.avatar.imageUrl.trim() || undefined,\n avatarId: fields.avatar.avatarId.trim() || undefined,\n })\n if (fields.type === 'transcription') {\n const temperature = Number(fields.transcription.temperature)\n Object.assign(body, {\n audioUrl: fields.transcription.audioUrl.trim(),\n language: fields.transcription.language.trim() || undefined,\n responseFormat: fields.transcription.responseFormat,\n // omit (let the API default) rather than serialize NaN → null on bad input\n temperature: Number.isFinite(temperature) ? temperature : undefined,\n })\n }\n return body\n}\n\nexport function generationStatus(generation: Generation): GenerationStatus {\n const metadata = generation.metadata ?? {}\n const status = typeof metadata.generationStatus === 'string' ? metadata.generationStatus : ''\n if (status === 'pending' || status === 'running' || status === 'failed' || status === 'succeeded') return status\n return generation.result ? 'succeeded' : 'pending'\n}\n\nexport function generationError(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n if (typeof metadata.providerError === 'string' && metadata.providerError.trim()) {\n return userSafeGenerationMessage(metadata.providerError)\n }\n if (typeof metadata.storageError === 'string' && metadata.storageError.trim()) {\n return metadata.storageError\n }\n return null\n}\n\nfunction generationClientRequestId(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n return typeof metadata.clientRequestId === 'string' && metadata.clientRequestId.trim()\n ? metadata.clientRequestId\n : null\n}\n\nfunction generationBatchSlotKey(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n const batchId = typeof metadata.batchId === 'string' && metadata.batchId.trim() ? metadata.batchId : null\n return batchId && typeof metadata.outputIndex === 'number'\n ? `${batchId}:${metadata.outputIndex}`\n : null\n}\n\nexport function generationMergeKey(generation: Generation): string | null {\n return generationBatchSlotKey(generation) ?? generationClientRequestId(generation)\n}\n\nexport function mergeLiveGeneration(current: Generation[], generation: Generation): Generation[] {\n const mergeKey = generationMergeKey(generation)\n const existingIndex = current.findIndex((item) => (\n item.id === generation.id\n || (mergeKey && generationMergeKey(item) === mergeKey)\n ))\n if (existingIndex === -1) return [generation, ...current]\n\n const next = [...current]\n next[existingIndex] = generation\n return next\n}\n\n// Overlay in-flight `live` generations on the loader's rows: each live row leads\n// (prefer the matching loader row by merge key / id so it carries the freshest\n// server state), then the remaining loader rows that no live row already\n// represents — deduped by BOTH id and merge key so a server row and its\n// optimistic twin never both appear. Returns `loader` unchanged when nothing is\n// live. Drives the canvas, library, and polling off one list.\nexport function mergeLoaderAndLive(loader: Generation[], live: Generation[]): Generation[] {\n if (live.length === 0) return loader\n const leading = live.map((generation) => {\n const mergeKey = generationMergeKey(generation)\n return mergeKey\n ? loader.find((gen) => generationMergeKey(gen) === mergeKey) ?? generation\n : loader.find((gen) => gen.id === generation.id) ?? generation\n })\n const leadingIds = new Set(leading.map((gen) => gen.id))\n const leadingMergeKeys = new Set(leading\n .map((gen) => generationMergeKey(gen))\n .filter((id): id is string => Boolean(id)))\n return [\n ...leading,\n ...loader.filter((gen) => (\n !leadingIds.has(gen.id)\n && !leadingMergeKeys.has(generationMergeKey(gen) ?? '')\n )),\n ]\n}\n\nexport function isLocalGeneration(generation: Generation): boolean {\n return generation.id.startsWith('local-')\n}\n\nfunction generationOutputIndex(generation: Generation): number {\n const value = generation.metadata?.outputIndex\n return typeof value === 'number' ? value : 0\n}\n\n// The most-recent run: all generations sharing the leading item's clientRequestId\n// (a multi-image batch), ordered by output slot. Falls back to the single leading\n// item when no request id is present. Drives the result canvas.\nexport function latestBatchOf(generations: Generation[]): Generation[] {\n const first = generations[0]\n if (!first) return []\n const key = generationClientRequestId(first)\n const batch = key\n ? generations.filter((generation) => generationClientRequestId(generation) === key)\n : [first]\n return [...batch].sort((a, b) => generationOutputIndex(a) - generationOutputIndex(b))\n}\n\nexport function userSafeGenerationMessage(message?: string): string {\n if (!message) return 'Generation failed'\n if (/Tangle API key is invalid or expired/i.test(message)) return message\n if (/(api[_ -]?key|secret|token|credential|env|configured|configuration)/i.test(message)) {\n return 'Generation failed'\n }\n return message\n}\n\nexport function optimisticGeneration({\n type,\n prompt,\n model,\n clientRequestId,\n outputIndex,\n outputCount,\n}: {\n type: GenerationType\n prompt: string\n model?: string\n clientRequestId: string\n outputIndex?: number\n outputCount?: number\n}): Generation {\n const batchId = outputIndex == null ? undefined : clientRequestId\n return {\n id: outputIndex == null ? `local-${clientRequestId}` : `local-${clientRequestId}-${outputIndex}`,\n type,\n prompt,\n result: null,\n model: model ?? null,\n cost: null,\n createdAt: new Date(),\n metadata: {\n generationStatus: 'pending',\n provider: type,\n clientRequestId,\n batchId,\n outputIndex,\n outputCount,\n },\n }\n}\n\nexport function failedOptimisticGeneration(generation: Generation): Generation {\n return {\n ...generation,\n metadata: {\n ...(generation.metadata ?? {}),\n generationStatus: 'failed',\n providerError: 'Generation failed',\n },\n }\n}\n\nexport function normalizeImageCount(value: unknown): number {\n const numeric = typeof value === 'number' ? value : Number(value)\n if (!Number.isFinite(numeric)) return MIN_IMAGE_COUNT\n return Math.min(Math.max(Math.trunc(numeric), MIN_IMAGE_COUNT), MAX_IMAGE_COUNT)\n}\n"],"mappings":"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","names":[]}