create-nextblock 0.11.1 → 0.11.3

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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
  3. package/templates/nextblock-template/app/actions/interactions.ts +372 -0
  4. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
  5. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
  6. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
  7. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
  8. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +951 -0
  9. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
  10. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +7 -2
  12. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +4 -0
  13. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
  14. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
  15. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
  16. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
  17. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
  18. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
  19. package/templates/nextblock-template/app/page.tsx +2 -2
  20. package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
  21. package/templates/nextblock-template/components/AppShell.tsx +1 -1
  22. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
  23. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
  24. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
  25. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
  26. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
  27. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
  28. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +7 -0
  29. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
  30. package/templates/nextblock-template/lib/setup/actions.ts +3 -1
  31. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +40 -0
  32. package/templates/nextblock-template/lib/updates/check-upstream.ts +38 -4
  33. package/templates/nextblock-template/package.json +2 -1
  34. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
  35. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
  36. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
  37. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
  38. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  39. package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
  40. package/templates/nextblock-template/lib/ai-client.ts +0 -247
  41. package/templates/nextblock-template/lib/ai-config.ts +0 -98
  42. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
  43. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
  44. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
  45. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
  46. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
  47. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
  48. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
  49. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
  50. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
  51. package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
  52. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
  53. package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
  54. package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
  55. package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
  56. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
  57. package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
  58. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
  59. 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
- }