@@ -5,15 +5,14 @@ import { useI18n } from '../i18n';
55import { listPlugins } from '../state/projects' ;
66import {
77 buildCommunityTemplates ,
8- copyTemplatePrompt ,
98 isPromptArtifact ,
10- templateActionLabel ,
119 TEMPLATE_TYPE_LABEL_KEY ,
1210 TEMPLATE_TYPE_ORDER ,
1311 type TemplateDemo ,
1412 type TemplateType ,
1513} from './CommunityTemplatePreview' ;
1614import { MediaSurface } from './plugins-home/cards/MediaSurface' ;
15+ import { canDuplicatePluginPreview } from './plugins-home/duplicate' ;
1716import { PluginDetailsModal } from './PluginDetailsModal' ;
1817import type { PluginUseAction } from './plugins-home/useActions' ;
1918import { useInView } from './plugins-home/useInView' ;
@@ -102,9 +101,9 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
102101 const [ detailsRecord , setDetailsRecord ] = useState < InstalledPluginRecord | null > ( null ) ;
103102 const [ activeType , setActiveType ] = useState < TemplateType > ( 'Slides' ) ;
104103 const [ activeSubtype , setActiveSubtype ] = useState ( 'All' ) ;
105- // Remix (and the prompt-artifact copy path it shares) hands off to a
106- // fire-and-forget parent callback (`onRemixTemplate`/`onUsePrompt` return
107- // void) that kicks off a real POST /api/projects — nothing here observes
104+ // Remix hands off to a fire-and-forget parent callback
105+ // (`onRemixTemplate` returns void) that kicks off a real POST /api/projects
106+ // — nothing here observes
108107 // when it settles. Without a guard, N rapid clicks before the resulting
109108 // navigation actually leaves this view fired N separate creates,
110109 // duplicating the project N times ("Community 的模板 remix 点击多次会复制
@@ -147,6 +146,10 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
147146 ( ) => buildCommunityTemplates ( plugins , locale , t , workspaceContext ) ,
148147 [ plugins , locale , t , workspaceContext ] ,
149148 ) ;
149+ const pluginById = useMemo (
150+ ( ) => new Map ( plugins . map ( ( record ) => [ record . id , record ] ) ) ,
151+ [ plugins ] ,
152+ ) ;
150153 const typeOptions = TEMPLATE_TYPE_ORDER . filter ( ( type ) =>
151154 templates . some ( ( template ) => template . type === type ) ,
152155 ) ;
@@ -165,19 +168,6 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
165168 return sourceKind === 'bundled' || sourceKind === 'marketplace' ? 'official' as const : 'personal' as const ;
166169 } ;
167170 const handleTemplateAction = ( template : TemplateDemo ) => {
168- if ( isPromptArtifact ( template ) ) {
169- trackCommunityTemplateClick ( analytics . track , {
170- page_name : 'community' ,
171- area : 'community_templates' ,
172- element : 'copy_prompt' ,
173- template_key : template . id ,
174- template_type : template . type ,
175- resource_scope : templateScope ( template . id ) ,
176- ...workspaceDimensions ,
177- } ) ;
178- void copyTemplatePrompt ( template ) ;
179- return ;
180- }
181171 // Synchronous check-and-set on the ref: this is what actually decides
182172 // whether a request goes out. See the remixingIdRef comment above for
183173 // why the state flag alone cannot gate this.
@@ -195,6 +185,28 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
195185 setRemixingId ( template . id ) ;
196186 onRemixTemplate ?.( { templateId : template . id , prompt : template . prompt } ) ;
197187 } ;
188+ const handleCardUse = ( template : TemplateDemo ) => {
189+ const target = templateUseTarget ( template ) ;
190+ trackCommunityTemplateClick ( analytics . track , {
191+ page_name : 'community' ,
192+ area : 'community_templates' ,
193+ element : 'use_prompt' ,
194+ template_key : template . id ,
195+ template_type : template . type ,
196+ resource_scope : templateScope ( template . id ) ,
197+ ...workspaceDimensions ,
198+ } ) ;
199+ const record = pluginById . get ( template . id ) ;
200+ if ( record && onUsePlugin ) {
201+ onUsePlugin ( record , 'use-with-query' , target ) ;
202+ return ;
203+ }
204+ onUsePrompt ?.( target ) ;
205+ } ;
206+ const canRemixTemplate = ( template : TemplateDemo ) => {
207+ const record = pluginById . get ( template . id ) ;
208+ return ! isPromptArtifact ( template ) && Boolean ( record && canDuplicatePluginPreview ( record ) ) ;
209+ } ;
198210 const templateById = useCallback (
199211 ( id : string ) => templates . find ( ( template ) => template . id === id ) ?? null ,
200212 [ templates ] ,
@@ -217,7 +229,7 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
217229 /** The detail modal's Use split action. Shells that own a Home hand-off
218230 * route the plugin as the composer's active driver; without one, fall back
219231 * to seeding the composer with the template's prompt (same destination the
220- * card's own prompt button uses). */
232+ * card's own Use button uses). */
221233 const handleDetailsUse = ( record : InstalledPluginRecord , action : PluginUseAction ) => {
222234 setDetailsRecord ( null ) ;
223235 const template = templateById ( record . id ) ;
@@ -330,7 +342,7 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
330342 onClick = { ( ) => openTemplateDetails ( template ) }
331343 >
332344 < div
333- className = " community-template-card__preview"
345+ className = { ` community-template-card__preview${ template . type === 'Slides' ? ' is-deck' : '' } ` }
334346 style = { { '--template-accent' : template . accent } as CSSProperties }
335347 aria-hidden
336348 >
@@ -339,34 +351,27 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
339351 < footer className = "community-template-card__foot" >
340352 < span > { template . meta } </ span >
341353 < div className = "community-template-card__actions" >
342- < button
343- type = "button"
344- disabled = { remixingId === template . id }
345- onClick = { ( event ) => {
346- event . stopPropagation ( ) ;
347- handleTemplateAction ( template ) ;
348- } }
349- >
350- { remixingId === template . id ? t ( 'common.loading' ) : templateActionLabel ( template ) }
351- </ button >
354+ { canRemixTemplate ( template ) ? (
355+ < button
356+ type = "button"
357+ disabled = { remixingId === template . id }
358+ onClick = { ( event ) => {
359+ event . stopPropagation ( ) ;
360+ handleTemplateAction ( template ) ;
361+ } }
362+ >
363+ { remixingId === template . id ? t ( 'common.loading' ) : 'Remix' }
364+ </ button >
365+ ) : null }
352366 < button
353367 type = "button"
354368 className = "community-template-card__prompt-btn"
355369 onClick = { ( event ) => {
356370 event . stopPropagation ( ) ;
357- trackCommunityTemplateClick ( analytics . track , {
358- page_name : 'community' ,
359- area : 'community_templates' ,
360- element : 'use_prompt' ,
361- template_key : template . id ,
362- template_type : template . type ,
363- resource_scope : templateScope ( template . id ) ,
364- ...workspaceDimensions ,
365- } ) ;
366- onUsePrompt ?.( templateUseTarget ( template ) ) ;
371+ handleCardUse ( template ) ;
367372 } }
368373 >
369- { t ( 'community.usePrompt ' ) }
374+ { t ( 'pluginCard.use ' ) }
370375 </ button >
371376 </ div >
372377 </ footer >
0 commit comments