@tuturuuu/ai 0.0.10
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 +76 -0
- package/package.json +106 -0
- package/src/api-key-hash.ts +28 -0
- package/src/calendar/events.ts +34 -0
- package/src/calendar/route.ts +114 -0
- package/src/chat/credit-source.ts +1 -0
- package/src/chat/google/chat-request-schema.ts +150 -0
- package/src/chat/google/default-system-instruction.ts +198 -0
- package/src/chat/google/message-file-processing.ts +212 -0
- package/src/chat/google/mira-step-preparation.ts +221 -0
- package/src/chat/google/new/route.ts +368 -0
- package/src/chat/google/route-auth.ts +81 -0
- package/src/chat/google/route-chat-resolution.ts +98 -0
- package/src/chat/google/route-credits.ts +61 -0
- package/src/chat/google/route-message-preparation.ts +331 -0
- package/src/chat/google/route-mira-runtime.ts +206 -0
- package/src/chat/google/route.ts +632 -0
- package/src/chat/google/stream-finish-persistence.ts +722 -0
- package/src/chat/google/summary/route.ts +153 -0
- package/src/chat/mira-render-ui-policy.ts +540 -0
- package/src/chat/mira-system-instruction.ts +484 -0
- package/src/chat-sdk/adapters.ts +389 -0
- package/src/chat-sdk/registry.ts +197 -0
- package/src/chat-sdk.ts +33 -0
- package/src/core.ts +3 -0
- package/src/credits/cap-output-tokens.ts +90 -0
- package/src/credits/check-credits.ts +232 -0
- package/src/credits/constants.ts +30 -0
- package/src/credits/index.ts +46 -0
- package/src/credits/model-mapping.ts +92 -0
- package/src/credits/reservations.ts +514 -0
- package/src/credits/resolve-plan-model.ts +219 -0
- package/src/credits/sync-gateway-models.ts +351 -0
- package/src/credits/types.ts +109 -0
- package/src/credits/use-ai-credits.ts +3 -0
- package/src/embeddings/metered.ts +283 -0
- package/src/executions/route.ts +137 -0
- package/src/generate/route.ts +411 -0
- package/src/hooks.ts +7 -0
- package/src/meetings/summary/route.ts +7 -0
- package/src/meetings/transcription/route.ts +134 -0
- package/src/memory/client.ts +158 -0
- package/src/memory/config.ts +38 -0
- package/src/memory/index.ts +32 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/middleware.ts +35 -0
- package/src/memory/operations.ts +480 -0
- package/src/memory/scope.ts +102 -0
- package/src/memory/settings.ts +121 -0
- package/src/memory/types.ts +101 -0
- package/src/memory/workspace.ts +36 -0
- package/src/memory.ts +1 -0
- package/src/mind/patch.ts +146 -0
- package/src/mind/route.ts +687 -0
- package/src/mind/tools.ts +1500 -0
- package/src/mind/types.ts +20 -0
- package/src/object/core.ts +3 -0
- package/src/object/flashcards/route.ts +140 -0
- package/src/object/quizzes/explanation/route.ts +145 -0
- package/src/object/quizzes/route.ts +142 -0
- package/src/object/types.ts +187 -0
- package/src/object/year-plan/route.ts +196 -0
- package/src/react.ts +1 -0
- package/src/scheduling/algorithm.ts +791 -0
- package/src/scheduling/default.ts +36 -0
- package/src/scheduling/duration-optimizer.ts +689 -0
- package/src/scheduling/index.ts +79 -0
- package/src/scheduling/priority-calculator.ts +187 -0
- package/src/scheduling/recurrence-calculator.ts +621 -0
- package/src/scheduling/templates.ts +892 -0
- package/src/scheduling/types.ts +136 -0
- package/src/scheduling/web-adapter.ts +308 -0
- package/src/scheduling.ts +6 -0
- package/src/supported-actions.ts +1 -0
- package/src/supported-providers.ts +6 -0
- package/src/tools/context-builder.ts +372 -0
- package/src/tools/core.ts +1 -0
- package/src/tools/definitions/calendar.ts +106 -0
- package/src/tools/definitions/finance.ts +197 -0
- package/src/tools/definitions/image.ts +74 -0
- package/src/tools/definitions/memory.ts +83 -0
- package/src/tools/definitions/meta.ts +154 -0
- package/src/tools/definitions/render-ui.ts +81 -0
- package/src/tools/definitions/tasks.ts +343 -0
- package/src/tools/definitions/time-tracking.ts +381 -0
- package/src/tools/definitions/workspace-context.ts +45 -0
- package/src/tools/definitions/workspace-user-chat.ts +111 -0
- package/src/tools/executors/calendar.ts +371 -0
- package/src/tools/executors/chat.ts +15 -0
- package/src/tools/executors/finance.ts +638 -0
- package/src/tools/executors/helpers/encryption.ts +107 -0
- package/src/tools/executors/image.ts +247 -0
- package/src/tools/executors/markitdown.ts +684 -0
- package/src/tools/executors/memory.ts +277 -0
- package/src/tools/executors/parallel-checks.ts +176 -0
- package/src/tools/executors/qr.ts +170 -0
- package/src/tools/executors/scope-helpers.ts +192 -0
- package/src/tools/executors/search.ts +149 -0
- package/src/tools/executors/settings.ts +40 -0
- package/src/tools/executors/tasks.ts +1087 -0
- package/src/tools/executors/theme.ts +23 -0
- package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
- package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
- package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
- package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
- package/src/tools/executors/timer/timer-helpers.ts +372 -0
- package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
- package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
- package/src/tools/executors/timer/timer-mutations.ts +19 -0
- package/src/tools/executors/timer/timer-queries.ts +18 -0
- package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
- package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
- package/src/tools/executors/timer/timer-session-queries.ts +153 -0
- package/src/tools/executors/timer/timer-session-updates.ts +200 -0
- package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
- package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
- package/src/tools/executors/timer.ts +22 -0
- package/src/tools/executors/user.ts +60 -0
- package/src/tools/executors/workspace.ts +135 -0
- package/src/tools/json-render-catalog.ts +875 -0
- package/src/tools/mira-tool-definitions.ts +55 -0
- package/src/tools/mira-tool-dispatcher.ts +265 -0
- package/src/tools/mira-tool-metadata.ts +164 -0
- package/src/tools/mira-tool-names.ts +95 -0
- package/src/tools/mira-tool-render-ui.ts +54 -0
- package/src/tools/mira-tool-types.ts +17 -0
- package/src/tools/mira-tools.ts +167 -0
- package/src/tools/normalize-render-ui-input.ts +321 -0
- package/src/tools/workspace-context.ts +233 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared encryption/decryption helpers for AI tool executors.
|
|
3
|
+
*
|
|
4
|
+
* Uses dynamic imports to avoid pulling server-only modules into the
|
|
5
|
+
* `packages/ai` dependency graph at build time. This mirrors the pattern
|
|
6
|
+
* used by the image generation tool for `createAdminClient`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type CalendarEventRow = {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
location?: string | null;
|
|
14
|
+
is_encrypted?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieve the workspace encryption key (read-only).
|
|
20
|
+
* Returns `null` when E2EE is not configured for the workspace.
|
|
21
|
+
*/
|
|
22
|
+
export async function getWorkspaceKeyForTools(
|
|
23
|
+
wsId: string
|
|
24
|
+
): Promise<Buffer | null> {
|
|
25
|
+
try {
|
|
26
|
+
const { isEncryptionEnabled, getMasterKey, decryptWorkspaceKey } =
|
|
27
|
+
await import('@tuturuuu/utils/encryption');
|
|
28
|
+
|
|
29
|
+
if (!isEncryptionEnabled()) return null;
|
|
30
|
+
|
|
31
|
+
const { createAdminClient } = await import(
|
|
32
|
+
'@tuturuuu/supabase/next/server'
|
|
33
|
+
);
|
|
34
|
+
const sbAdmin = await createAdminClient();
|
|
35
|
+
const masterKey = getMasterKey();
|
|
36
|
+
|
|
37
|
+
const { data, error } = await sbAdmin
|
|
38
|
+
.from('workspace_encryption_keys')
|
|
39
|
+
.select('encrypted_key')
|
|
40
|
+
.eq('ws_id', wsId)
|
|
41
|
+
.maybeSingle();
|
|
42
|
+
|
|
43
|
+
if (error || !data) return null;
|
|
44
|
+
|
|
45
|
+
return await decryptWorkspaceKey(
|
|
46
|
+
(data as { encrypted_key: string }).encrypted_key,
|
|
47
|
+
masterKey
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt an array of calendar events in-place if any are encrypted.
|
|
56
|
+
*/
|
|
57
|
+
export async function decryptEventsForTools<T extends CalendarEventRow>(
|
|
58
|
+
events: T[],
|
|
59
|
+
wsId: string
|
|
60
|
+
): Promise<T[]> {
|
|
61
|
+
const hasEncrypted = events.some((e) => e.is_encrypted);
|
|
62
|
+
if (!hasEncrypted) return events;
|
|
63
|
+
|
|
64
|
+
const key = await getWorkspaceKeyForTools(wsId);
|
|
65
|
+
if (!key) return events;
|
|
66
|
+
|
|
67
|
+
const { decryptCalendarEvents } = await import('@tuturuuu/utils/encryption');
|
|
68
|
+
return decryptCalendarEvents(events, key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Encrypt the sensitive fields of a calendar event before storage.
|
|
73
|
+
* Returns the original data untouched when E2EE is not enabled.
|
|
74
|
+
*/
|
|
75
|
+
export async function encryptEventFieldsForTools(
|
|
76
|
+
fields: { title: string; description: string; location: string | null },
|
|
77
|
+
wsId: string
|
|
78
|
+
): Promise<{
|
|
79
|
+
title: string;
|
|
80
|
+
description: string;
|
|
81
|
+
location: string | null;
|
|
82
|
+
is_encrypted: boolean;
|
|
83
|
+
}> {
|
|
84
|
+
const key = await getWorkspaceKeyForTools(wsId);
|
|
85
|
+
if (!key) {
|
|
86
|
+
return { ...fields, is_encrypted: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { encryptCalendarEventFields } = await import(
|
|
90
|
+
'@tuturuuu/utils/encryption'
|
|
91
|
+
);
|
|
92
|
+
const encrypted = encryptCalendarEventFields(
|
|
93
|
+
{
|
|
94
|
+
title: fields.title,
|
|
95
|
+
description: fields.description,
|
|
96
|
+
location: fields.location ?? undefined,
|
|
97
|
+
},
|
|
98
|
+
key
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
title: encrypted.title,
|
|
103
|
+
description: encrypted.description,
|
|
104
|
+
location: encrypted.location ?? null,
|
|
105
|
+
is_encrypted: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { google } from '@ai-sdk/google';
|
|
2
|
+
import { isGoogleModelId, toBareModelName } from '../../credits/model-mapping';
|
|
3
|
+
import {
|
|
4
|
+
PlanModelResolutionError,
|
|
5
|
+
resolvePlanModel,
|
|
6
|
+
} from '../../credits/resolve-plan-model';
|
|
7
|
+
import type { MiraToolContext } from '../mira-tools';
|
|
8
|
+
|
|
9
|
+
export async function executeGenerateImage(
|
|
10
|
+
args: Record<string, unknown>,
|
|
11
|
+
ctx: MiraToolContext
|
|
12
|
+
): Promise<unknown> {
|
|
13
|
+
const billingWsId = ctx.creditWsId ?? ctx.wsId;
|
|
14
|
+
const prompt = args.prompt as string;
|
|
15
|
+
const aspectRatio = (args.aspectRatio as string) ?? '1:1';
|
|
16
|
+
let selectedModel: string;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const resolvedModel = await resolvePlanModel({
|
|
20
|
+
capability: 'image',
|
|
21
|
+
requestedModel:
|
|
22
|
+
typeof args.model === 'string' ? (args.model as string) : undefined,
|
|
23
|
+
wsId: billingWsId,
|
|
24
|
+
});
|
|
25
|
+
selectedModel = resolvedModel.modelId;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof PlanModelResolutionError) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: error.message,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { checkAiCredits } = await import('../../credits/check-credits');
|
|
38
|
+
const {
|
|
39
|
+
commitFixedAiCreditReservation,
|
|
40
|
+
releaseFixedAiCreditReservation,
|
|
41
|
+
reserveFixedAiCredits,
|
|
42
|
+
} = await import('../../credits/reservations');
|
|
43
|
+
let commitResult: Awaited<
|
|
44
|
+
ReturnType<typeof commitFixedAiCreditReservation>
|
|
45
|
+
> | null = null;
|
|
46
|
+
const { createAdminClient } = await import('@tuturuuu/supabase/next/server');
|
|
47
|
+
const creditCheck = await checkAiCredits(
|
|
48
|
+
billingWsId,
|
|
49
|
+
selectedModel,
|
|
50
|
+
'image_generation',
|
|
51
|
+
{ userId: ctx.userId }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!creditCheck.allowed) {
|
|
55
|
+
const errorMessages: Record<string, string> = {
|
|
56
|
+
FEATURE_NOT_ALLOWED:
|
|
57
|
+
'Image generation is not available on your current plan.',
|
|
58
|
+
MODEL_NOT_ALLOWED: `The model ${selectedModel} is not enabled for your workspace.`,
|
|
59
|
+
CREDITS_EXHAUSTED: 'You have run out of AI credits for image generation.',
|
|
60
|
+
NO_ALLOCATION: 'Image generation is not configured for your workspace.',
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error:
|
|
65
|
+
errorMessages[creditCheck.errorCode ?? ''] ??
|
|
66
|
+
'Image generation is not available. Please check your AI credit settings.',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sbAdmin = await createAdminClient();
|
|
71
|
+
const reservationMetadata = {
|
|
72
|
+
aspectRatio,
|
|
73
|
+
model: selectedModel,
|
|
74
|
+
feature: 'image_generation',
|
|
75
|
+
};
|
|
76
|
+
// Full metadata including prompt for storage — never logged directly.
|
|
77
|
+
const fullReservationMetadata = {
|
|
78
|
+
prompt,
|
|
79
|
+
wsId: billingWsId,
|
|
80
|
+
userId: ctx.userId,
|
|
81
|
+
...reservationMetadata,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const reservation = await reserveFixedAiCredits(
|
|
85
|
+
{
|
|
86
|
+
wsId: billingWsId,
|
|
87
|
+
userId: ctx.userId,
|
|
88
|
+
amount: 1,
|
|
89
|
+
modelId: selectedModel,
|
|
90
|
+
feature: 'image_generation',
|
|
91
|
+
metadata: fullReservationMetadata,
|
|
92
|
+
},
|
|
93
|
+
sbAdmin
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!reservation.success || !reservation.reservationId) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error:
|
|
100
|
+
reservation.errorCode === 'INSUFFICIENT_CREDITS'
|
|
101
|
+
? 'You have run out of AI credits for image generation.'
|
|
102
|
+
: 'Failed to reserve AI credits for image generation.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { generateImage, gateway } = await import('ai');
|
|
107
|
+
|
|
108
|
+
const imageId = crypto.randomUUID();
|
|
109
|
+
const storagePath = `${ctx.wsId}/mira/images/${imageId}.png`;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const model = isGoogleModelId(selectedModel)
|
|
113
|
+
? google.image(toBareModelName(selectedModel))
|
|
114
|
+
: gateway.image(selectedModel);
|
|
115
|
+
const { image } = await generateImage({
|
|
116
|
+
model,
|
|
117
|
+
prompt,
|
|
118
|
+
aspectRatio: aspectRatio as `${number}:${number}`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { error: uploadError } = await sbAdmin.storage
|
|
122
|
+
.from('workspaces')
|
|
123
|
+
.upload(storagePath, image.uint8Array, {
|
|
124
|
+
contentType: 'image/png',
|
|
125
|
+
upsert: false,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (uploadError) {
|
|
129
|
+
throw new Error(`Upload failed: ${uploadError.message}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { data: urlData, error: urlError } = await sbAdmin.storage
|
|
133
|
+
.from('workspaces')
|
|
134
|
+
.createSignedUrl(storagePath, 60 * 60 * 24 * 30);
|
|
135
|
+
|
|
136
|
+
if (urlError || !urlData) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Signed URL failed: ${urlError?.message ?? 'No data returned'}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
commitResult = await commitFixedAiCreditReservation(
|
|
143
|
+
reservation.reservationId,
|
|
144
|
+
{
|
|
145
|
+
...fullReservationMetadata,
|
|
146
|
+
storagePath,
|
|
147
|
+
},
|
|
148
|
+
sbAdmin
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!commitResult.success) {
|
|
152
|
+
throw new Error('Failed to finalize AI credit deduction.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
imageUrl: urlData.signedUrl,
|
|
158
|
+
storagePath,
|
|
159
|
+
prompt,
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
let commitOrReleaseError: Error | null = null;
|
|
163
|
+
|
|
164
|
+
// Attempt to release the reservation first, with defensive error handling.
|
|
165
|
+
// Releasing before storage cleanup ensures we know the reservation's final
|
|
166
|
+
// state before deciding whether to keep or remove the image.
|
|
167
|
+
let releaseResult: Awaited<
|
|
168
|
+
ReturnType<typeof releaseFixedAiCreditReservation>
|
|
169
|
+
> | null = null;
|
|
170
|
+
try {
|
|
171
|
+
releaseResult = await releaseFixedAiCreditReservation(
|
|
172
|
+
reservation.reservationId,
|
|
173
|
+
{
|
|
174
|
+
...reservationMetadata,
|
|
175
|
+
storagePath,
|
|
176
|
+
error:
|
|
177
|
+
error instanceof Error
|
|
178
|
+
? error.message
|
|
179
|
+
: 'Unknown image generation error',
|
|
180
|
+
},
|
|
181
|
+
sbAdmin
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!releaseResult.success) {
|
|
185
|
+
console.error('Failed to release AI credit reservation', {
|
|
186
|
+
reservationId: reservation.reservationId,
|
|
187
|
+
storagePath,
|
|
188
|
+
releaseResult,
|
|
189
|
+
commitResult,
|
|
190
|
+
});
|
|
191
|
+
commitOrReleaseError = new Error(
|
|
192
|
+
`AI credit reservation release failed (${releaseResult.errorCode ?? 'UNKNOWN'}): ${JSON.stringify(
|
|
193
|
+
{
|
|
194
|
+
commitResult,
|
|
195
|
+
releaseResult,
|
|
196
|
+
}
|
|
197
|
+
)}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} catch (releaseError) {
|
|
201
|
+
console.error('Failed to release AI credit reservation', {
|
|
202
|
+
reservationId: reservation.reservationId,
|
|
203
|
+
storagePath,
|
|
204
|
+
releaseError:
|
|
205
|
+
releaseError instanceof Error
|
|
206
|
+
? releaseError.message
|
|
207
|
+
: String(releaseError),
|
|
208
|
+
});
|
|
209
|
+
commitOrReleaseError = new Error(
|
|
210
|
+
`Failed to release reservation: ${
|
|
211
|
+
releaseError instanceof Error
|
|
212
|
+
? releaseError.message
|
|
213
|
+
: 'Unknown release error'
|
|
214
|
+
}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Only delete the image if the credit commit did NOT succeed AND
|
|
219
|
+
// the reservation was not already committed (confirmed by release).
|
|
220
|
+
// If commit succeeded (e.g. HTTP response was lost), keep the image
|
|
221
|
+
// so the user isn't charged for nothing.
|
|
222
|
+
const alreadyCommitted =
|
|
223
|
+
releaseResult?.errorCode === 'RESERVATION_ALREADY_COMMITTED';
|
|
224
|
+
if (storagePath && !commitResult?.success && !alreadyCommitted) {
|
|
225
|
+
const { error: removeError } = await sbAdmin.storage
|
|
226
|
+
.from('workspaces')
|
|
227
|
+
.remove([storagePath]);
|
|
228
|
+
if (removeError) {
|
|
229
|
+
console.error('Failed to cleanup image upload', {
|
|
230
|
+
storagePath,
|
|
231
|
+
error: removeError.message,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
error: commitOrReleaseError?.message
|
|
239
|
+
? `${commitOrReleaseError.message} ${
|
|
240
|
+
error instanceof Error ? `Original error: ${error.message}` : ''
|
|
241
|
+
}`.trim()
|
|
242
|
+
: error instanceof Error
|
|
243
|
+
? error.message
|
|
244
|
+
: 'Image generation failed. Please try again.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|