create-nextblock 0.10.9 → 0.11.2
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/package.json +1 -1
- package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
- package/templates/nextblock-template/app/actions/interactions.ts +372 -0
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +837 -0
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -0
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -0
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +18 -13
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
- package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
- package/templates/nextblock-template/app/page.tsx +2 -2
- package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
- package/templates/nextblock-template/components/AppShell.tsx +1 -1
- package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +9 -8
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +38 -9
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
- package/templates/nextblock-template/lib/onboarding/status.ts +13 -6
- package/templates/nextblock-template/lib/setup/actions.ts +3 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +30 -0
- package/templates/nextblock-template/lib/updates/check-upstream.ts +44 -7
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -0
- package/templates/nextblock-template/lib/updates/repo-identity.ts +11 -1
- package/templates/nextblock-template/package.json +2 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
- package/templates/nextblock-template/lib/ai-client.ts +0 -247
- package/templates/nextblock-template/lib/ai-config.ts +0 -98
- package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
- package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
- package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
- package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
- package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
- package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
- package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
- package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
- package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
- package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
- package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
- package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
- package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
- package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
- package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
- package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
- package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { generateObject } from 'ai';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildCortexAiRoutingPolicy,
|
|
5
|
-
createCortexAiOpenRouterClient,
|
|
6
|
-
} from './ai-client';
|
|
7
|
-
import {
|
|
8
|
-
getHttpStatusCode,
|
|
9
|
-
isOpenRouterRecoverableRoutingError,
|
|
10
|
-
omitUnsupportedCortexAiModelOptions,
|
|
11
|
-
runWithCortexAiModelFallback,
|
|
12
|
-
type CortexAiModelAttempt,
|
|
13
|
-
type CortexAiOpenRouterModelId,
|
|
14
|
-
type CortexAiStoredModelSelection,
|
|
15
|
-
} from './ai-model-registry';
|
|
16
|
-
import {
|
|
17
|
-
buildCortexWidgetBuilderPrompt,
|
|
18
|
-
buildCortexWidgetBuilderSystemPrompt,
|
|
19
|
-
cortexWidgetBuildRequestSchema,
|
|
20
|
-
validateCortexWidgetDefinitionOutput,
|
|
21
|
-
type CortexWidgetDefinition,
|
|
22
|
-
type CortexWidgetBuildRequest,
|
|
23
|
-
} from './cortex-widget-schema';
|
|
24
|
-
|
|
25
|
-
export type GenerateCortexWidgetDefinitionResult = {
|
|
26
|
-
attempts: readonly CortexAiModelAttempt[];
|
|
27
|
-
credentialSource: 'env' | 'stored' | 'manual';
|
|
28
|
-
definition: CortexWidgetDefinition;
|
|
29
|
-
modelId: CortexAiOpenRouterModelId;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const CORTEX_WIDGET_BUILD_ATTEMPT_TIMEOUT_MS = 60_000;
|
|
33
|
-
|
|
34
|
-
function isRecoverableCortexWidgetBuildError(error: unknown) {
|
|
35
|
-
const statusCode = getHttpStatusCode(error);
|
|
36
|
-
|
|
37
|
-
if (statusCode === 401 || statusCode === 402 || statusCode === 403) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (isOpenRouterRecoverableRoutingError(error)) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (statusCode && statusCode >= 500) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
-
return /NoObjectGenerated|No object generated|structured output|schema|validation|Invalid|aborted|abort|timeout|timed out/i.test(
|
|
51
|
-
message
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function generateCortexWidgetDefinition(
|
|
56
|
-
params: CortexWidgetBuildRequest & {
|
|
57
|
-
apiKey?: string;
|
|
58
|
-
fallbackModelIds?: readonly CortexAiOpenRouterModelId[];
|
|
59
|
-
modelId?: CortexAiOpenRouterModelId;
|
|
60
|
-
modelSelection?: CortexAiStoredModelSelection | null;
|
|
61
|
-
}
|
|
62
|
-
): Promise<GenerateCortexWidgetDefinitionResult> {
|
|
63
|
-
const { apiKey, fallbackModelIds, modelId, modelSelection, ...requestParams } = params;
|
|
64
|
-
const request = cortexWidgetBuildRequestSchema.parse(requestParams);
|
|
65
|
-
const client = await createCortexAiOpenRouterClient({ apiKey, modelSelection });
|
|
66
|
-
const routingPolicy = buildCortexAiRoutingPolicy({
|
|
67
|
-
credentialSource: client.credentialSource,
|
|
68
|
-
fallbackModelIds,
|
|
69
|
-
requestedModelId: modelId ?? request.modelId,
|
|
70
|
-
selectedModel: client.modelSelection,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const generation = await runWithCortexAiModelFallback({
|
|
74
|
-
modelIds: routingPolicy.modelIds,
|
|
75
|
-
shouldRetry: isRecoverableCortexWidgetBuildError,
|
|
76
|
-
execute: async (attemptModelId) => {
|
|
77
|
-
const abortController = new AbortController();
|
|
78
|
-
const timeoutId = setTimeout(
|
|
79
|
-
() => abortController.abort(),
|
|
80
|
-
CORTEX_WIDGET_BUILD_ATTEMPT_TIMEOUT_MS
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const attemptOptions = omitUnsupportedCortexAiModelOptions(
|
|
85
|
-
{
|
|
86
|
-
abortSignal: abortController.signal,
|
|
87
|
-
maxOutputTokens: 7000,
|
|
88
|
-
maxRetries: 0,
|
|
89
|
-
// Use JSON mode WITHOUT a provider-side schema. The widget definition
|
|
90
|
-
// is a recursive, discriminated-union structure; sending it as a
|
|
91
|
-
// response_format json_schema is rejected by Google Gemini
|
|
92
|
-
// ("reference to undefined schema", recursion not supported) and by
|
|
93
|
-
// OpenAI ("'oneOf' is not permitted"). We instead describe the shape
|
|
94
|
-
// in the prompt and validate the returned JSON with our own Zod
|
|
95
|
-
// schema below, which works across every OpenRouter model.
|
|
96
|
-
output: 'no-schema',
|
|
97
|
-
prompt: buildCortexWidgetBuilderPrompt(request),
|
|
98
|
-
system: buildCortexWidgetBuilderSystemPrompt(),
|
|
99
|
-
temperature: 0.15,
|
|
100
|
-
} as Record<string, unknown>,
|
|
101
|
-
{
|
|
102
|
-
modelId: attemptModelId,
|
|
103
|
-
modelSelection: routingPolicy.modelSelection,
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const result = await generateObject({
|
|
108
|
-
...attemptOptions,
|
|
109
|
-
model: client.model(attemptModelId),
|
|
110
|
-
} as Parameters<typeof generateObject>[0]);
|
|
111
|
-
|
|
112
|
-
return validateCortexWidgetDefinitionOutput(result.object);
|
|
113
|
-
} finally {
|
|
114
|
-
clearTimeout(timeoutId);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
attempts: generation.attempts,
|
|
121
|
-
credentialSource: client.credentialSource,
|
|
122
|
-
definition: generation.result,
|
|
123
|
-
modelId: generation.modelId,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
|
|
3
|
-
import { z } from './zod-config';
|
|
4
|
-
|
|
5
|
-
// The widget-builder pipeline (and its heavier zod/AI deps) is imported lazily
|
|
6
|
-
// inside the execute functions so that merely importing this tools module stays
|
|
7
|
-
// light — important because some test suites mock @nextblock-cms/utils with a
|
|
8
|
-
// partial export set.
|
|
9
|
-
|
|
10
|
-
// Custom block definitions are GLOBAL (a reusable block library), not scoped to a
|
|
11
|
-
// page/post/product. These tools let Cortex AI create/edit/delete them directly,
|
|
12
|
-
// reusing the constrained-decoding widget builder so the agent never has to
|
|
13
|
-
// hand-author a recursive layout tree.
|
|
14
|
-
|
|
15
|
-
type CustomBlockToolContext = {
|
|
16
|
-
actorUserId?: string | null;
|
|
17
|
-
cortexAiApiKey?: string | null;
|
|
18
|
-
cortexAiModelSelection?: unknown;
|
|
19
|
-
latestUserMessage?: string | null;
|
|
20
|
-
skipConfirmation?: boolean;
|
|
21
|
-
supabase?: { from: (table: string) => any };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const CUSTOM_BLOCK_SELECT = 'id, slug, name, description, fields, layout_schema, is_original';
|
|
25
|
-
|
|
26
|
-
export const createCustomBlockInputSchema = z.strictObject({
|
|
27
|
-
context: z
|
|
28
|
-
.string()
|
|
29
|
-
.trim()
|
|
30
|
-
.max(3000)
|
|
31
|
-
.optional()
|
|
32
|
-
.describe('Optional extra constraints or brand/style guidance for the generated block.'),
|
|
33
|
-
prompt: z
|
|
34
|
-
.string()
|
|
35
|
-
.trim()
|
|
36
|
-
.min(3)
|
|
37
|
-
.max(4000)
|
|
38
|
-
.describe(
|
|
39
|
-
'Natural-language description of the custom block, including the fields it needs and the visual style. Example: "A product card with a title, image, price, and a button that links to the product page."'
|
|
40
|
-
),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export const updateCustomBlockInputSchema = z.strictObject({
|
|
44
|
-
prompt: z
|
|
45
|
-
.string()
|
|
46
|
-
.trim()
|
|
47
|
-
.min(3)
|
|
48
|
-
.max(4000)
|
|
49
|
-
.describe('Description of the changes to apply. The block is regenerated using its existing definition as context.'),
|
|
50
|
-
slug: z.string().trim().min(1).max(120).describe('Slug of the existing custom block to edit.'),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export const deleteCustomBlockInputSchema = z.strictObject({
|
|
54
|
-
slug: z.string().trim().min(1).max(120).describe('Slug of the custom block definition to delete.'),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export const listCustomBlocksInputSchema = z.strictObject({
|
|
58
|
-
query: z.string().trim().max(120).optional().describe('Optional text filter on name or slug.'),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
function requireSupabase(context?: CustomBlockToolContext) {
|
|
62
|
-
if (!context?.supabase) {
|
|
63
|
-
throw new Error('A Supabase service client is required to manage custom block definitions.');
|
|
64
|
-
}
|
|
65
|
-
return context.supabase;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function requireActor(context?: CustomBlockToolContext) {
|
|
69
|
-
if (!context?.actorUserId) {
|
|
70
|
-
throw new Error('Managing custom block definitions requires an authenticated admin actor.');
|
|
71
|
-
}
|
|
72
|
-
return context.actorUserId;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toJson(value: unknown) {
|
|
76
|
-
return JSON.parse(JSON.stringify(value));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function summarizeDefinition(definition: any) {
|
|
80
|
-
const fields = Array.isArray(definition?.fields) ? definition.fields : [];
|
|
81
|
-
return {
|
|
82
|
-
fieldCount: fields.length,
|
|
83
|
-
fields: fields.map((field: any) => ({ key: field?.key, label: field?.label, type: field?.type })),
|
|
84
|
-
id: definition?.id,
|
|
85
|
-
name: definition?.name,
|
|
86
|
-
slug: definition?.slug,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function buildWidgetGenerationParams(input: { prompt: string; context?: string }, context?: CustomBlockToolContext) {
|
|
91
|
-
const apiKey = context?.cortexAiApiKey || undefined;
|
|
92
|
-
return {
|
|
93
|
-
apiKey,
|
|
94
|
-
context: input.context,
|
|
95
|
-
modelSelection: apiKey && context?.cortexAiModelSelection ? (context.cortexAiModelSelection as any) : undefined,
|
|
96
|
-
prompt: input.prompt,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function revalidateCustomBlockCaches(definition?: { id?: string; slug?: string } | null) {
|
|
101
|
-
try {
|
|
102
|
-
// Lazily required so the module stays import-safe outside a request scope.
|
|
103
|
-
const { revalidatePath, revalidateTag } = require('next/cache') as typeof import('next/cache');
|
|
104
|
-
revalidateTag('custom-block-definitions', 'max');
|
|
105
|
-
if (definition?.id) revalidateTag(`custom-block-definitions:${definition.id}`, 'max');
|
|
106
|
-
if (definition?.slug) revalidateTag(`custom-block-definitions:${definition.slug}`, 'max');
|
|
107
|
-
revalidateTag('dynamic-layout-engine', 'max');
|
|
108
|
-
revalidatePath('/cms/custom-blocks');
|
|
109
|
-
revalidatePath('/cms/blocks');
|
|
110
|
-
} catch {
|
|
111
|
-
// Revalidation is best-effort; the 60s definition cache still refreshes.
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function normalizeConfirmation(value: string | null | undefined) {
|
|
116
|
-
return (value || '').replace(/\s+/g, ' ').trim().toUpperCase();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function buildDeleteConfirmationPhrase(slug: string) {
|
|
120
|
-
return `CONFIRM DELETE CUSTOM BLOCK ${slug.toUpperCase()}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isDeleteConfirmed(context: CustomBlockToolContext | undefined, phrase: string) {
|
|
124
|
-
if (context?.skipConfirmation) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return normalizeConfirmation(context?.latestUserMessage).includes(normalizeConfirmation(phrase));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function serializeError(error: unknown) {
|
|
131
|
-
if (error && typeof error === 'object') {
|
|
132
|
-
const candidate = error as { code?: string; message?: string };
|
|
133
|
-
if (candidate.code === '23505') {
|
|
134
|
-
return 'A custom block with that slug already exists. Choose a different name or edit the existing one.';
|
|
135
|
-
}
|
|
136
|
-
if (typeof candidate.message === 'string' && candidate.message) {
|
|
137
|
-
return candidate.message;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (error instanceof Error) {
|
|
141
|
-
return error.message;
|
|
142
|
-
}
|
|
143
|
-
return 'Unknown error.';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function executeCreateCustomBlock(
|
|
147
|
-
input: z.infer<typeof createCustomBlockInputSchema>,
|
|
148
|
-
context?: CustomBlockToolContext
|
|
149
|
-
) {
|
|
150
|
-
try {
|
|
151
|
-
const supabase = requireSupabase(context);
|
|
152
|
-
requireActor(context);
|
|
153
|
-
|
|
154
|
-
const [{ generateCortexWidgetDefinition }, { insertCortexWidgetDefinition }] = await Promise.all([
|
|
155
|
-
import('./ai-cortex-widget-builder'),
|
|
156
|
-
import('./cortex-widget-registry'),
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
const generation = await generateCortexWidgetDefinition(buildWidgetGenerationParams(input, context));
|
|
160
|
-
const definition = await insertCortexWidgetDefinition(supabase as any, generation.definition);
|
|
161
|
-
|
|
162
|
-
revalidateCustomBlockCaches(definition);
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
definition: summarizeDefinition(definition),
|
|
166
|
-
editUrl: `/cms/custom-blocks/${definition.id}/edit`,
|
|
167
|
-
mutationExecuted: true,
|
|
168
|
-
success: true,
|
|
169
|
-
};
|
|
170
|
-
} catch (error) {
|
|
171
|
-
return { mutationExecuted: false, message: serializeError(error), success: false };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function executeUpdateCustomBlock(
|
|
176
|
-
input: z.infer<typeof updateCustomBlockInputSchema>,
|
|
177
|
-
context?: CustomBlockToolContext
|
|
178
|
-
) {
|
|
179
|
-
try {
|
|
180
|
-
const supabase = requireSupabase(context);
|
|
181
|
-
requireActor(context);
|
|
182
|
-
|
|
183
|
-
const { data: existing } = await supabase
|
|
184
|
-
.from('custom_block_definitions')
|
|
185
|
-
.select(CUSTOM_BLOCK_SELECT)
|
|
186
|
-
.eq('slug', input.slug)
|
|
187
|
-
.maybeSingle();
|
|
188
|
-
|
|
189
|
-
if (!existing) {
|
|
190
|
-
return { mutationExecuted: false, message: `No custom block found with slug "${input.slug}".`, success: false };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const [{ generateCortexWidgetDefinition }, { customBlockDefinitionCreateSchema }] = await Promise.all([
|
|
194
|
-
import('./ai-cortex-widget-builder'),
|
|
195
|
-
import('@nextblock-cms/utils'),
|
|
196
|
-
]);
|
|
197
|
-
|
|
198
|
-
const generation = await generateCortexWidgetDefinition(
|
|
199
|
-
buildWidgetGenerationParams(
|
|
200
|
-
{
|
|
201
|
-
context: `You are editing an existing custom block named "${existing.name}". Keep its overall purpose and only apply the requested changes. Existing definition: ${JSON.stringify(
|
|
202
|
-
{ fields: existing.fields, layout_schema: existing.layout_schema, name: existing.name }
|
|
203
|
-
)}`,
|
|
204
|
-
prompt: input.prompt,
|
|
205
|
-
},
|
|
206
|
-
context
|
|
207
|
-
)
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// Preserve the existing slug + provenance so placed instances keep resolving.
|
|
211
|
-
const parsed = customBlockDefinitionCreateSchema.parse({
|
|
212
|
-
...generation.definition,
|
|
213
|
-
is_original: existing.is_original,
|
|
214
|
-
slug: existing.slug,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const { data, error } = await supabase
|
|
218
|
-
.from('custom_block_definitions')
|
|
219
|
-
.update({
|
|
220
|
-
description: parsed.description,
|
|
221
|
-
fields: toJson(parsed.fields),
|
|
222
|
-
layout_schema: toJson(parsed.layout_schema),
|
|
223
|
-
name: parsed.name,
|
|
224
|
-
})
|
|
225
|
-
.eq('id', existing.id)
|
|
226
|
-
.select(CUSTOM_BLOCK_SELECT)
|
|
227
|
-
.single();
|
|
228
|
-
|
|
229
|
-
if (error || !data) {
|
|
230
|
-
return { mutationExecuted: false, message: error?.message ?? 'Failed to update custom block.', success: false };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
revalidateCustomBlockCaches(data);
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
definition: summarizeDefinition(data),
|
|
237
|
-
editUrl: `/cms/custom-blocks/${existing.id}/edit`,
|
|
238
|
-
mutationExecuted: true,
|
|
239
|
-
success: true,
|
|
240
|
-
};
|
|
241
|
-
} catch (error) {
|
|
242
|
-
return { mutationExecuted: false, message: serializeError(error), success: false };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export async function executeDeleteCustomBlock(
|
|
247
|
-
input: z.infer<typeof deleteCustomBlockInputSchema>,
|
|
248
|
-
context?: CustomBlockToolContext
|
|
249
|
-
) {
|
|
250
|
-
try {
|
|
251
|
-
const supabase = requireSupabase(context);
|
|
252
|
-
requireActor(context);
|
|
253
|
-
|
|
254
|
-
const { data: existing } = await supabase
|
|
255
|
-
.from('custom_block_definitions')
|
|
256
|
-
.select('id, slug, name')
|
|
257
|
-
.eq('slug', input.slug)
|
|
258
|
-
.maybeSingle();
|
|
259
|
-
|
|
260
|
-
if (!existing) {
|
|
261
|
-
return { mutationExecuted: false, message: `No custom block found with slug "${input.slug}".`, success: false };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const confirmationPhrase = buildDeleteConfirmationPhrase(existing.slug);
|
|
265
|
-
|
|
266
|
-
if (!isDeleteConfirmed(context, confirmationPhrase)) {
|
|
267
|
-
return {
|
|
268
|
-
confirmationPhrase,
|
|
269
|
-
mutationExecuted: false,
|
|
270
|
-
preview: {
|
|
271
|
-
summary: `Delete the custom block "${existing.name}" (${existing.slug}). Pages still using it will stop rendering it until replaced. This cannot be undone.`,
|
|
272
|
-
},
|
|
273
|
-
requiresConfirmation: true,
|
|
274
|
-
success: true,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const { error } = await supabase.from('custom_block_definitions').delete().eq('id', existing.id);
|
|
279
|
-
|
|
280
|
-
if (error) {
|
|
281
|
-
return { mutationExecuted: false, message: `Failed to delete custom block: ${error.message}`, success: false };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
revalidateCustomBlockCaches(existing);
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
deleted: { name: existing.name, slug: existing.slug },
|
|
288
|
-
mutationExecuted: true,
|
|
289
|
-
success: true,
|
|
290
|
-
};
|
|
291
|
-
} catch (error) {
|
|
292
|
-
return { mutationExecuted: false, message: serializeError(error), success: false };
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function executeListCustomBlocks(
|
|
297
|
-
input: z.infer<typeof listCustomBlocksInputSchema>,
|
|
298
|
-
context?: CustomBlockToolContext
|
|
299
|
-
) {
|
|
300
|
-
try {
|
|
301
|
-
const supabase = requireSupabase(context);
|
|
302
|
-
|
|
303
|
-
const { data, error } = await supabase
|
|
304
|
-
.from('custom_block_definitions')
|
|
305
|
-
.select('id, slug, name, description, fields, is_original')
|
|
306
|
-
.order('name', { ascending: true });
|
|
307
|
-
|
|
308
|
-
if (error) {
|
|
309
|
-
return { message: error.message, success: false };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
let blocks = Array.isArray(data) ? data : [];
|
|
313
|
-
if (input.query) {
|
|
314
|
-
const needle = input.query.toLowerCase();
|
|
315
|
-
blocks = blocks.filter(
|
|
316
|
-
(definition: any) =>
|
|
317
|
-
String(definition.name || '').toLowerCase().includes(needle) ||
|
|
318
|
-
String(definition.slug || '').toLowerCase().includes(needle)
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
blocks: blocks.map((definition: any) => summarizeDefinition(definition)),
|
|
324
|
-
count: blocks.length,
|
|
325
|
-
success: true,
|
|
326
|
-
};
|
|
327
|
-
} catch (error) {
|
|
328
|
-
return { message: serializeError(error), success: false };
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export function createCortexCustomBlockTools(context?: CustomBlockToolContext) {
|
|
333
|
-
return {
|
|
334
|
-
create_custom_block: tool({
|
|
335
|
-
description:
|
|
336
|
-
'Create a brand-new reusable custom block definition from a natural-language description (for example "a product card with title, image, price, and a button linking to the product page"). This is a GLOBAL block-library builder and does NOT require an open page, post, or product. It generates the field schema and Tailwind layout and saves it so the block can be added to any page afterward. Additive and reversible; executes immediately without a confirmation phrase.',
|
|
337
|
-
execute: (input) => executeCreateCustomBlock(input, context),
|
|
338
|
-
inputSchema: createCustomBlockInputSchema,
|
|
339
|
-
strict: true,
|
|
340
|
-
}),
|
|
341
|
-
delete_custom_block: tool({
|
|
342
|
-
description:
|
|
343
|
-
'Delete a custom block definition by slug. Mutating: first returns a confirmation phrase; only executes after the user replies with the exact phrase. Use list_custom_blocks first if you are unsure of the slug.',
|
|
344
|
-
execute: (input) => executeDeleteCustomBlock(input, context),
|
|
345
|
-
inputSchema: deleteCustomBlockInputSchema,
|
|
346
|
-
strict: true,
|
|
347
|
-
}),
|
|
348
|
-
list_custom_blocks: tool({
|
|
349
|
-
description:
|
|
350
|
-
'List the existing custom block definitions (slug, name, and fields). Read-only and does not require page context. Use it to find the slug of a block to edit or delete.',
|
|
351
|
-
execute: (input) => executeListCustomBlocks(input, context),
|
|
352
|
-
inputSchema: listCustomBlocksInputSchema,
|
|
353
|
-
strict: true,
|
|
354
|
-
}),
|
|
355
|
-
update_custom_block: tool({
|
|
356
|
-
description:
|
|
357
|
-
'Edit an existing custom block definition (identified by slug) from a new natural-language description. The block is regenerated with its current definition as context and keeps its slug so existing placements keep working. Executes immediately.',
|
|
358
|
-
execute: (input) => executeUpdateCustomBlock(input, context),
|
|
359
|
-
inputSchema: updateCustomBlockInputSchema,
|
|
360
|
-
strict: true,
|
|
361
|
-
}),
|
|
362
|
-
};
|
|
363
|
-
}
|