diff --git a/packages/pen-core/src/element-builders/image-placeholder-v1.ts b/packages/pen-core/src/element-builders/image-placeholder-v1.ts index 6a07052ab..93417da44 100644 --- a/packages/pen-core/src/element-builders/image-placeholder-v1.ts +++ b/packages/pen-core/src/element-builders/image-placeholder-v1.ts @@ -25,6 +25,14 @@ export interface ImagePlaceholderV1Params { * - `'system'`: emits `$color-*` refs for all fill fields. */ theme?: V1Theme; + /** + * 2-3 English keywords to search for when the auto-search pass replaces + * this placeholder with a real photo (e.g. "burger fries", "modern + * office"). Stamped onto the frame as `imageSearchQuery`; consumed by + * the web app's `image-search-pipeline`. Without it the pipeline falls + * back to `label` then a generic "placeholder" query. + */ + image_search_query?: string; } /** @@ -73,7 +81,7 @@ export function buildImagePlaceholderV1(params: ImagePlaceholderV1Params): Eleme }); } - return { + const frame: ElementTree = { type: 'frame', name: 'Image Placeholder', role: 'image-placeholder', @@ -87,4 +95,9 @@ export function buildImagePlaceholderV1(params: ImagePlaceholderV1Params): Eleme fill: [{ type: 'solid', color: frameBg }], children, }; + if (typeof params.image_search_query === 'string' && params.image_search_query.length > 0) { + (frame as ElementTree & { imageSearchQuery?: string }).imageSearchQuery = + params.image_search_query; + } + return frame; } diff --git a/packages/pen-core/src/element-builders/image-placeholder.ts b/packages/pen-core/src/element-builders/image-placeholder.ts index 1ecf215db..3f324e193 100644 --- a/packages/pen-core/src/element-builders/image-placeholder.ts +++ b/packages/pen-core/src/element-builders/image-placeholder.ts @@ -20,6 +20,14 @@ export interface ImagePlaceholderParams { * placeholder, but `add_avatar_v0` is a cleaner fit for that. */ corner_radius?: number; + /** + * 2-3 English keywords to search for when the auto-search pass replaces + * this placeholder with a real photo (e.g. "burger fries", "modern + * office"). Stamped onto the frame as `imageSearchQuery`; consumed by + * the web app's `image-search-pipeline`. Without it the pipeline falls + * back to `label` then a generic "placeholder" query. + */ + image_search_query?: string; } /** @@ -72,7 +80,7 @@ export function buildImagePlaceholder(params: ImagePlaceholderParams): ElementTr }); } - return { + const frame: ElementTree = { type: 'frame', name: 'Image Placeholder', role: 'image-placeholder', @@ -86,4 +94,9 @@ export function buildImagePlaceholder(params: ImagePlaceholderParams): ElementTr fill: [{ type: 'solid', color: '#F1F5F9' }], children, }; + if (typeof params.image_search_query === 'string' && params.image_search_query.length > 0) { + (frame as ElementTree & { imageSearchQuery?: string }).imageSearchQuery = + params.image_search_query; + } + return frame; } diff --git a/packages/pen-mcp/src/__tests__/add-image-placeholder-v0.test.ts b/packages/pen-mcp/src/__tests__/add-image-placeholder-v0.test.ts index 7ddf75410..3dce41f61 100644 --- a/packages/pen-mcp/src/__tests__/add-image-placeholder-v0.test.ts +++ b/packages/pen-mcp/src/__tests__/add-image-placeholder-v0.test.ts @@ -108,4 +108,28 @@ describe('add_image_placeholder_v0', () => { ); expect(await readFile(fp, 'utf-8')).toBe(before); }); + + it('image_search_query is stamped onto the frame as imageSearchQuery', async () => { + const fp = await fresh('a.op'); + await handleAddImagePlaceholderV0({ + filePath: fp, + image_search_query: 'burger fries', + }); + const ph = getRoot(await readDoc(fp)); + expect(ph.imageSearchQuery).toBe('burger fries'); + }); + + it('omitting image_search_query leaves the field undefined', async () => { + const fp = await fresh('a.op'); + await handleAddImagePlaceholderV0({ filePath: fp }); + const ph = getRoot(await readDoc(fp)); + expect(ph.imageSearchQuery).toBeUndefined(); + }); + + it('empty-string image_search_query is treated as missing', async () => { + const fp = await fresh('a.op'); + await handleAddImagePlaceholderV0({ filePath: fp, image_search_query: '' }); + const ph = getRoot(await readDoc(fp)); + expect(ph.imageSearchQuery).toBeUndefined(); + }); }); diff --git a/packages/pen-mcp/src/routes/element-tool-defs-ext-2.ts b/packages/pen-mcp/src/routes/element-tool-defs-ext-2.ts index 7629009d8..9ece3410d 100644 --- a/packages/pen-mcp/src/routes/element-tool-defs-ext-2.ts +++ b/packages/pen-mcp/src/routes/element-tool-defs-ext-2.ts @@ -206,7 +206,9 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_2 = [ 'The "this will be an image later" affordance. Use for "photo slot", "hero image area", ' + '"upload zone", "cover placeholder", "图片占位". Separate from G() (which fetches real ' + 'images via search) — this emits a visual placeholder frame, not an image node. ' + - 'schemaVersion 1.0', + 'When the canvas runs its auto-search pass, an image_search_query (or label) lets it ' + + 'fetch a relevant photo to replace the gray box; without one it falls back to a ' + + 'generic stock photo. schemaVersion 1.0', inputSchema: { type: 'object' as const, properties: { @@ -226,6 +228,14 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_2 = [ type: 'number', description: 'Corner radius (default 8). Use larger for card-style, 0 for sharp.', }, + image_search_query: { + type: 'string', + description: + '2-3 English keywords for the auto-search pass to fetch a photo with ' + + '(e.g. "burger fries", "modern office workspace", "yoga sunset"). Strongly ' + + 'recommended for restaurant cards, product photos, hero banners, etc — ' + + 'without it the pipeline searches for the label or a generic placeholder.', + }, parent_id: parentIdProp, pageId: pageIdProp, }, diff --git a/packages/pen-mcp/src/routes/element-tool-defs-ext-6.ts b/packages/pen-mcp/src/routes/element-tool-defs-ext-6.ts index 701290bd2..c53b958d8 100644 --- a/packages/pen-mcp/src/routes/element-tool-defs-ext-6.ts +++ b/packages/pen-mcp/src/routes/element-tool-defs-ext-6.ts @@ -388,7 +388,9 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_6 = [ 'Theme-aware image placeholder (v1). theme="light" (default): byte-parity with ' + 'add_image_placeholder_v0. theme="dark": bg → bgDeep, icon → textMuted, label → textMuted. ' + 'theme="system": $color-* refs. Gray box + centered icon + optional caption. ' + - 'schemaVersion 1.0', + 'When the canvas runs its auto-search pass, an image_search_query (or label) lets ' + + 'it fetch a relevant photo to replace the gray box; without one it falls back to a ' + + 'generic stock photo. schemaVersion 1.0', inputSchema: { type: 'object' as const, properties: { @@ -404,6 +406,14 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_6 = [ enum: ['light', 'dark', 'system'], description: 'Theme variant. Default "light".', }, + image_search_query: { + type: 'string', + description: + '2-3 English keywords for the auto-search pass to fetch a photo with ' + + '(e.g. "burger fries", "modern office workspace", "yoga sunset"). Strongly ' + + 'recommended for restaurant cards, product photos, hero banners, etc — ' + + 'without it the pipeline searches for the label or a generic placeholder.', + }, parent_id: parentIdProp, pageId: pageIdProp, },