From 48f3404051b93e90682a802e9aba8516d7561d95 Mon Sep 17 00:00:00 2001 From: qiongyu1999 <2694684348@qq.com> Date: Thu, 21 May 2026 16:37:00 +0800 Subject: [PATCH] fix home example prompt presets --- apps/web/src/components/HomeHero.tsx | 24 ++++++++- apps/web/src/components/HomeView.tsx | 18 ++++--- .../tests/components/HomeHero.rail.test.tsx | 54 ++++++++++++++++++- .../components/HomeView.prefill.test.tsx | 49 +++++++++++++++++ .../open-design.json | 2 +- .../template.json | 2 +- .../notion-team-dashboard-live-artifact.json | 2 +- 7 files changed, 136 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/HomeHero.tsx b/apps/web/src/components/HomeHero.tsx index 03665c5ce..3efc6fbe6 100644 --- a/apps/web/src/components/HomeHero.tsx +++ b/apps/web/src/components/HomeHero.tsx @@ -2556,11 +2556,29 @@ function homeHeroExamplePluginsForChip( plugins: InstalledPluginRecord[], locale: Locale, ): InstalledPluginRecord[] { - return plugins + const presets = plugins .filter((plugin) => pluginMatchesExampleChip(plugin, chipId)) .filter((plugin) => Boolean(pluginPresetQuery(plugin, locale))) .sort((a, b) => pluginPresetRank(b, chipId) - pluginPresetRank(a, chipId)) .slice(0, 18); + if (chipId === 'image') { + return movePluginPresetToEnd(presets, 'example-hatch-pet'); + } + return presets; +} + +function movePluginPresetToEnd( + records: InstalledPluginRecord[], + pluginId: string, +): InstalledPluginRecord[] { + const index = records.findIndex((record) => record.id === pluginId); + if (index < 0 || index === records.length - 1) return records; + const record = records[index]!; + return [ + ...records.slice(0, index), + ...records.slice(index + 1), + record, + ]; } function pluginMatchesExampleChip(record: InstalledPluginRecord, chipId: string): boolean { @@ -2579,8 +2597,10 @@ function pluginMatchesExampleChip(record: InstalledPluginRecord, chipId: string) return has('deck', 'slides', 'slide-deck') || hasPart('slide', 'deck'); case 'hyperframes': return hasPart('hyperframes', 'hyperframe'); + case 'live-artifact': + return has('live-artifact') || hasPart('live-artifact'); case 'image': - return (has('image') || hasPart('image-template')) && !hasPart('video', 'audio'); + return (has('image') || hasPart('image-template')) && !hasPart('video', 'audio', 'live-artifact'); case 'video': return (has('video') || hasPart('video-template')) && !hasPart('hyperframes', 'audio'); case 'audio': diff --git a/apps/web/src/components/HomeView.tsx b/apps/web/src/components/HomeView.tsx index 2c0007f41..e69b3e7a7 100644 --- a/apps/web/src/components/HomeView.tsx +++ b/apps/web/src/components/HomeView.tsx @@ -860,14 +860,16 @@ export function HomeView({ function useExamplePlugin(record: InstalledPluginRecord, chipId: string, promptText: string) { const projectKind = projectKindForExamplePlugin(record, chipId); - requestActivePlugin(record, promptText, { - projectKind, - chipId, - inputs: {}, - inputFields: [], - queryTemplate: null, - replaceWithoutConfirmation: true, - }); + activePluginApplyRequestRef.current += 1; + setActive(null); + setActiveSkill(null); + setFallbackProjectKind(projectKind); + setPendingApplyId(null); + setPendingChipId(null); + setError(null); + setPrompt(promptText); + setPromptEditedByUser(false); + focusPromptAtEnd(); } function removePluginContext(pluginId: string) { diff --git a/apps/web/tests/components/HomeHero.rail.test.tsx b/apps/web/tests/components/HomeHero.rail.test.tsx index 7ee72fbd4..875c72bdd 100644 --- a/apps/web/tests/components/HomeHero.rail.test.tsx +++ b/apps/web/tests/components/HomeHero.rail.test.tsx @@ -22,7 +22,12 @@ afterEach(() => { cleanup(); }); -function makePlugin(id: string, mode: string, title = id): InstalledPluginRecord { +function makePlugin( + id: string, + mode: string, + title = id, + extraTags: string[] = [], +): InstalledPluginRecord { return { id, title, @@ -36,7 +41,7 @@ function makePlugin(id: string, mode: string, title = id): InstalledPluginRecord version: '1.0.0', title, description: 'Plugin preset fixture', - tags: [mode], + tags: [mode, ...extraTags], od: { mode, useCase: { @@ -167,6 +172,51 @@ describe('HomeHero intent rail', () => { ); }); + it('keeps Hatch Pet at the end of the image example presets', () => { + const hatchPet = makePlugin('example-hatch-pet', 'image', 'Hatch Pet'); + const imagePoster = makePlugin('image-template-poster', 'image', 'Image Poster'); + const stoneInfographic = makePlugin('image-template-stone', 'image', 'Stone Infographic'); + renderHero({ + activeChipId: 'image', + pluginOptions: [hatchPet, imagePoster, stoneInfographic], + }); + + const presets = screen.getAllByTestId('home-hero-plugin-preset'); + expect(presets.map((preset) => preset.textContent)).toEqual([ + expect.stringContaining('Image Poster'), + expect.stringContaining('Stone Infographic'), + expect.stringContaining('Hatch Pet'), + ]); + }); + + it('moves live artifact presets out of Image and into Live artifact examples', () => { + const imagePoster = makePlugin('image-template-poster', 'image', 'Image Poster'); + const notionDashboard = makePlugin( + 'image-template-notion-team-dashboard-live-artifact', + 'image', + 'Notion-style Team Dashboard (Live Artifact)', + ['live-artifact'], + ); + renderHero({ + activeChipId: 'image', + pluginOptions: [imagePoster, notionDashboard], + }); + + let presets = screen.getAllByTestId('home-hero-plugin-preset'); + expect(presets).toHaveLength(1); + expect(presets[0]?.textContent).toContain('Image Poster'); + + cleanup(); + renderHero({ + activeChipId: 'live-artifact', + pluginOptions: [imagePoster, notionDashboard], + }); + + presets = screen.getAllByTestId('home-hero-plugin-preset'); + expect(presets).toHaveLength(1); + expect(presets[0]?.textContent).toContain('Notion-style Team Dashboard (Live Artifact)'); + }); + it('disables every visible chip while a plugin apply is in flight', () => { renderHero({ pendingPluginId: 'od-figma-migration', pendingChipId: 'figma' }); for (const chip of HOME_HERO_CHIPS.filter((item) => item.group === 'create')) { diff --git a/apps/web/tests/components/HomeView.prefill.test.tsx b/apps/web/tests/components/HomeView.prefill.test.tsx index 8425c2c46..dd378a020 100644 --- a/apps/web/tests/components/HomeView.prefill.test.tsx +++ b/apps/web/tests/components/HomeView.prefill.test.tsx @@ -581,6 +581,55 @@ describe('HomeView prompt handoff', () => { expect(screen.queryByRole('alert')).toBeNull(); }); + it('uses example preset cards as plain-text prompt fillers without binding plugin inputs', async () => { + const fetchMock = vi.fn(async (url) => { + if (typeof url === 'string' && url === '/api/plugins') { + return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + if (typeof url === 'string' && url.includes('/apply')) { + return new Response(JSON.stringify(WEB_PROTOTYPE_APPLY_RESULT), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + throw new Error(`unexpected fetch ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + stubAnimationFrame(); + + render( + undefined} + onOpenProject={() => undefined} + onViewAllProjects={() => undefined} + />, + ); + + await clearActiveTypeChip(); + fireEvent.click(await screen.findByTestId('home-hero-rail-prototype')); + fireEvent.click(await screen.findByTestId('home-hero-plugin-preset')); + + const input = screen.getByTestId('home-hero-input') as HTMLTextAreaElement; + await waitFor(() => { + expect(input.value).toBe( + 'Build a high-fidelity web prototype for product evaluators using the active project design system from the bundled web prototype seed.', + ); + }); + expect(fetchMock.mock.calls.some(([url]) => ( + typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply') + ))).toBe(false); + expect(screen.queryByTestId('home-hero-active-type-chip')).toBeNull(); + expect(screen.queryByTestId('plugin-inputs-form')).toBeNull(); + expect(screen.queryByTestId('home-hero-prompt-slot-fidelity')).toBeNull(); + expect(screen.queryByTestId('home-hero-prompt-slot-artifactKind')).toBeNull(); + expect(screen.queryByTestId('home-hero-prompt-slot-designSystem')).toBeNull(); + expect(screen.queryByTestId('home-hero-prompt-slot-template')).toBeNull(); + }); + it('binds the Home rail Live artifact chip with live-artifact metadata and applies it on submit', async () => { const fetchMock = vi.fn(async (url) => { if (typeof url === 'string' && url === '/api/plugins') { diff --git a/plugins/_official/image-templates/notion-team-dashboard-live-artifact/open-design.json b/plugins/_official/image-templates/notion-team-dashboard-live-artifact/open-design.json index ed95739c6..d01f8c912 100644 --- a/plugins/_official/image-templates/notion-team-dashboard-live-artifact/open-design.json +++ b/plugins/_official/image-templates/notion-team-dashboard-live-artifact/open-design.json @@ -27,7 +27,7 @@ "surface": "image", "preview": { "type": "image", - "poster": "https://raw.githubusercontent.com/joeylee12629-star/open-design/feat/prompt-template-live-artifact-dashboard/prompt-templates/image/notion-team-dashboard-live-artifact.preview.png" + "poster": "https://raw.githubusercontent.com/nexu-io/open-design/main/prompt-templates/image/notion-team-dashboard-live-artifact.preview.png" }, "useCase": { "query": { diff --git a/plugins/_official/image-templates/notion-team-dashboard-live-artifact/template.json b/plugins/_official/image-templates/notion-team-dashboard-live-artifact/template.json index f66587267..df645de2c 100644 --- a/plugins/_official/image-templates/notion-team-dashboard-live-artifact/template.json +++ b/plugins/_official/image-templates/notion-team-dashboard-live-artifact/template.json @@ -12,7 +12,7 @@ "model": "gpt-image-2", "aspect": "4:3", "prompt": "{\n \"type\": \"team productivity dashboard screenshot (prompt-only design preview, no live connector data)\",\n \"ui_aesthetic\": \"Notion native — off-white background #FFFFFF with #F7F6F3 sidebar, 14px SF Pro / Inter body, charcoal ink #37352F, hairline grid #ECECEA, accent blue #2EAADC used sparingly. No gradients, no card shadows, no rounded inner cards, no glassmorphism, no purple→pink hero, no emoji icon strip across the top.\",\n \"top_banner\": {\n \"color\": \"soft amber #FDECC8 with #E6CF94 hairline\",\n \"text\": \"Sample data — design preview. This page is a prompt-only Notion-style dashboard mockup; every number, name, and timestamp below is seeded, not pulled from a real Notion workspace or Composio connector. For real refreshable / connector-backed artifacts, see the live-artifact skill.\"\n },\n \"topbar\": {\n \"breadcrumb\": \"{argument name=\\\"workspace name\\\" default=\\\"Acme Studio\\\"} / Workspace / {argument name=\\\"page title\\\" default=\\\"Team Dashboard\\\"}\",\n \"preview_pill\": \"pill on the right reading 'Sample · design preview' with a small neutral-grey dot — do NOT render a 'Live · synced', 'Online', or any live/sync pill\"\n },\n \"page_header\": {\n \"page_emoji\": \"📊 (a single semantically relevant Notion-style emoji, not 🚀 / ✨ / 🔥)\",\n \"title\": \"{argument name=\\\"page title\\\" default=\\\"Team Dashboard\\\"} rendered at 40px weight 700, letter-spacing -0.01em\",\n \"meta_row\": \"Last edited by {argument name=\\\"editor name\\\" default=\\\"Sarah Chen\\\"} · Seeded sample data — for real refreshable / connector-backed runs use the live-artifact skill. Do NOT render a 'Last refreshed just now' label, an 'Auto' toggle, or a 'Refresh from Notion' button anywhere in this row.\"\n },\n \"callout\": \"💡 This is a prompt-only design preview. The numbers, names, and timestamps below are seeded sample data — they are not pulled from a real Notion workspace and not refreshed via the Composio connector. For real refreshable / connector-backed Live Artifacts, use the live-artifact skill.\",\n \"kpi_grid\": {\n \"count\": \"{argument name=\\\"kpi count\\\" default=\\\"4\\\"}\",\n \"items\": [\n {\n \"label\": \"Total tasks\",\n \"value\": \"143\",\n \"delta\": \"↑ 6 vs last week (green)\"\n },\n {\n \"label\": \"Done this week\",\n \"value\": \"24\",\n \"delta\": \"↑ 4 vs last week (green)\"\n },\n {\n \"label\": \"Active members\",\n \"value\": \"11 / 14\",\n \"delta\": \"· Stable (grey)\"\n },\n {\n \"label\": \"Docs awaiting review\",\n \"value\": \"7\",\n \"delta\": \"↓ 2 vs last week (red)\"\n }\n ],\n \"style\": \"1px hairline grid; no shadows; tabular-nums weight 600 numbers; small grey delta line under each KPI; uppercase 12px label-grey labels. All values are seeded sample data.\"\n },\n \"sparkline_card\": {\n \"title\": \"Tasks created · last 7 days (sample)\",\n \"total\": \"100 total (sample)\",\n \"shape\": \"hand-rolled SVG, 7 days Wed→Tue, 2px stroke accent blue, 10% alpha fill below the curve, very light dotted baseline grid\"\n },\n \"activity_feed_card\": {\n \"title\": \"Recent activity (sample)\",\n \"subtitle\": \"Notion-style seeded activity for design preview — not from a real Notion workspace\",\n \"rows\": \"5 rows shaped '<18px round colored avatar with 2-letter initials> ·