Skip to content

Commit 0511c4c

Browse files
authored
Merge pull request #2420 from objectstack-ai/feat/blueprint-gallery-gantt-views
feat(spec): gallery + gantt as first-class blueprint view kinds
2 parents 19c25d4 + 66d5932 commit 0511c4c

2 files changed

Lines changed: 37 additions & 7 deletions

File tree

packages/spec/src/ai/solution-blueprint.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ describe('SolutionBlueprintSchema', () => {
5353
expect(parsed.views?.[0].type).toBe('list');
5454
});
5555

56+
it('accepts gallery and gantt as first-class view kinds (not only list/form/kanban/calendar)', () => {
57+
// A build agent that wants a poster wall or a schedule must be able to PROPOSE
58+
// a gallery/gantt view directly — otherwise the closest allowed enum value
59+
// (list) wins and the view silently downgrades to a grid.
60+
const parsed = SolutionBlueprintSchema.parse({
61+
summary: 'events',
62+
objects: [{ name: 'event', fields: [{ name: 'name', type: 'text' }, { name: 'poster', type: 'image' }] }],
63+
views: [
64+
{ object: 'event', name: 'poster_wall', type: 'gallery', columns: ['poster', 'name'] },
65+
{ object: 'event', name: 'schedule', type: 'gantt', columns: ['name'] },
66+
],
67+
});
68+
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
69+
});
70+
5671
it('rejects a missing summary', () => {
5772
const { summary: _drop, ...noSummary } = validBlueprint;
5873
expect(() => SolutionBlueprintSchema.parse(noSummary)).toThrow();
@@ -204,6 +219,20 @@ describe('SolutionBlueprintStrictSchema (OpenAI strict mirror)', () => {
204219
expect(parsed.app).toBeNull();
205220
});
206221

222+
it('accepts gallery/gantt view kinds in the strict mirror (the structured-output contract)', () => {
223+
// This is the schema the build agent's structured output is validated against,
224+
// so the gallery/gantt enum values MUST live here too — else the model can
225+
// never emit them and a requested gallery degrades to a list/grid.
226+
const parsed = SolutionBlueprintStrictSchema.parse({
227+
...strictBp,
228+
views: [
229+
{ object: 'event', name: 'wall', label: null, type: 'gallery', columns: null, groupBy: null },
230+
{ object: 'event', name: 'plan', label: null, type: 'gantt', columns: null, groupBy: null },
231+
],
232+
});
233+
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
234+
});
235+
207236
it('requires every top-level key to be present (OpenAI strict needs all in `required`)', () => {
208237
const { views: _v, ...missingViews } = strictBp;
209238
expect(() => SolutionBlueprintStrictSchema.parse(missingViews)).toThrow();

packages/spec/src/ai/solution-blueprint.zod.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,17 @@ export const BlueprintObjectSchema = lazySchema(() => z.object({
5050
}));
5151
export type BlueprintObject = z.infer<typeof BlueprintObjectSchema>;
5252

53-
/** A proposed list/form/kanban/calendar view over an object. */
53+
/** A proposed list/form/kanban/calendar/gallery/gantt view over an object. */
5454
export const BlueprintViewSchema = lazySchema(() => z.object({
5555
object: z.string().regex(SNAKE_CASE).describe('Object this view displays (snake_case)'),
5656
name: z.string().regex(SNAKE_CASE).describe('View machine name (snake_case)'),
5757
label: z.string().optional().describe('Human-readable view label'),
58-
type: z.enum(['list', 'form', 'kanban', 'calendar']).default('list').describe('View kind'),
58+
type: z.enum(['list', 'form', 'kanban', 'calendar', 'gallery', 'gantt']).default('list')
59+
.describe('View kind. Pick the surface that fits the data: "gallery" for a visual card/cover browse when the user asks for a 画廊/相册/卡片墙/封面/海报/图集 (a gallery / card wall / cover / poster grid) or the object has an image/avatar/file field worth showing as a card cover; "gantt" for a 甘特图/时间线/排期 (timeline / schedule) when the object has BOTH a start and an end date field; "kanban" for a board grouped by a status/select field; "calendar" for a single-date schedule; "form" for a record editor; else "list".'),
5960
columns: z.array(z.string().regex(SNAKE_CASE)).optional()
60-
.describe('Field names shown as columns (in order)'),
61+
.describe('Field names shown as columns (in order). For a gallery, INCLUDE the image/avatar/file field (it becomes the card cover); for a gantt, INCLUDE the start date column before the end date column.'),
6162
groupBy: z.string().regex(SNAKE_CASE).optional()
62-
.describe('REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list.'),
63+
.describe('REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows).'),
6364
}));
6465
export type BlueprintView = z.infer<typeof BlueprintViewSchema>;
6566

@@ -203,9 +204,9 @@ const StrictView = z.object({
203204
object: z.string().describe('Object this view displays (snake_case)'),
204205
name: z.string().describe('View machine name (snake_case)'),
205206
label: z.string().nullable().describe('Human-readable view label, or null'),
206-
type: z.enum(['list', 'form', 'kanban', 'calendar']).nullable().describe('View kind, or null for list'),
207-
columns: z.array(z.string()).nullable().describe('Field names shown as columns, or null'),
208-
groupBy: z.string().nullable().describe('REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Null for non-kanban views.'),
207+
type: z.enum(['list', 'form', 'kanban', 'calendar', 'gallery', 'gantt']).nullable().describe('View kind, or null for list. "gallery" = visual card/cover browse (画廊/相册/卡片墙/封面/海报, or an object with an image/avatar/file field); "gantt" = timeline/schedule (甘特图/时间线/排期, object with BOTH a start and an end date field); "kanban" = board grouped by a status/select field; "calendar" = single-date schedule; "form" = record editor.'),
208+
columns: z.array(z.string()).nullable().describe('Field names shown as columns, or null. For a gallery, INCLUDE the image/avatar/file field (becomes the card cover); for a gantt, INCLUDE the start date column before the end date column.'),
209+
groupBy: z.string().nullable().describe('REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Optional for gantt (groups leaf tasks). Null for list/form/calendar/gallery.'),
209210
});
210211

211212
const StrictDashboard = z.object({

0 commit comments

Comments
 (0)