1313
1414import type { DesignSpec , Element } from '@next-dev/editor-core' ;
1515import { catalog , catalogToPrompt , type ComponentType } from '@next-dev/catalog' ;
16+ import {
17+ collectSubtreeElementIds ,
18+ parsePromptWithContext ,
19+ resolveSelectedElementTarget ,
20+ resolveValidationHintTarget ,
21+ } from '@/chat-context' ;
1622
1723// ─── Types ──────────────────────────────────────────────────────────────────
1824
@@ -109,7 +115,9 @@ Operation types: add (parentId, elementType, props), remove (elementId), updateP
109115Example - "Add a button that says Submit":
110116{"description":"Added Submit button","operations":[{"type":"add","parentId":"${ spec . root } ","elementType":"Button","props":{"children":"Submit","variant":"default"}}]}
111117
112- Rules: Use ONLY listed components. Return ONLY JSON. No markdown fences.` ;
118+ Rules: Use ONLY listed components. Return ONLY JSON. No markdown fences.
119+ If the user prompt contains a DesignForge chat context block with selected_element_id, treat relative edit requests as targeting that element first.
120+ If provider.validation is present in the context block, keep validation-related changes aligned with that provider.` ;
113121}
114122
115123// ─── Mock Provider ──────────────────────────────────────────────────────────
@@ -140,17 +148,24 @@ export class MockProvider implements AIProvider {
140148 }
141149
142150 private parse ( prompt : string , spec : DesignSpec ) : AIResponse {
143- const p = prompt . trim ( ) . toLowerCase ( ) ;
151+ const promptContext = parsePromptWithContext ( prompt ) ;
152+ const request = promptContext . request || prompt . trim ( ) ;
153+ const p = request . toLowerCase ( ) ;
144154 const rootId = spec . root ;
145155
146156 // Remove commands
147157 if ( p . startsWith ( 'remove' ) || p . startsWith ( 'delete' ) ) {
148- return this . handleRemove ( p , spec ) ;
158+ return this . handleRemove ( request , spec , promptContext . selectedElementId ) ;
149159 }
150160
151161 // Update commands
152162 if ( p . startsWith ( 'change' ) || p . startsWith ( 'update' ) || p . startsWith ( 'set' ) || p . startsWith ( 'make' ) ) {
153- return this . handleUpdate ( p , spec ) ;
163+ return this . handleUpdate (
164+ request ,
165+ spec ,
166+ promptContext . selectedElementId ,
167+ promptContext . providerSelection . validation ,
168+ ) ;
154169 }
155170
156171 // Login form
@@ -177,7 +192,7 @@ export class MockProvider implements AIProvider {
177192 if ( matchedType ) {
178193 const entry = catalog [ matchedType ] ;
179194 const defaultProps = extractDefaults ( entry . schema ) ;
180- const quoted = extractQuotedText ( prompt ) ;
195+ const quoted = extractQuotedText ( request ) ;
181196 if ( quoted && 'children' in defaultProps ) defaultProps . children = quoted ;
182197
183198 // Variant extraction
@@ -196,7 +211,7 @@ export class MockProvider implements AIProvider {
196211
197212 // Text fallback
198213 if ( p . includes ( 'text' ) || p . includes ( 'heading' ) || p . includes ( 'paragraph' ) ) {
199- const text = extractQuotedText ( prompt ) ?? 'Sample text' ;
214+ const text = extractQuotedText ( request ) ?? 'Sample text' ;
200215 let variant = 'default' ;
201216 if ( p . includes ( 'heading' ) || p . includes ( 'h1' ) ) variant = 'h1' ;
202217 else if ( p . includes ( 'h2' ) ) variant = 'h2' ;
@@ -215,13 +230,23 @@ export class MockProvider implements AIProvider {
215230 } ;
216231 }
217232
218- private handleRemove ( prompt : string , spec : DesignSpec ) : AIResponse {
233+ private handleRemove ( prompt : string , spec : DesignSpec , selectedElementId : string | null ) : AIResponse {
234+ const normalizedPrompt = prompt . toLowerCase ( ) ;
219235 const componentTypes = Object . keys ( catalog ) as ComponentType [ ] ;
220- const matched = componentTypes . find ( ( t ) => prompt . includes ( t . toLowerCase ( ) ) ) ;
236+ const matched = componentTypes . find ( ( t ) => normalizedPrompt . includes ( t . toLowerCase ( ) ) ) ;
237+ if ( ! matched && selectedElementId ) {
238+ const target = spec . elements [ selectedElementId ] ;
239+ if ( ! target ) return { operations : [ ] , description : 'Could not determine which element to remove.' } ;
240+ return {
241+ operations : [ { type : 'remove' , elementId : selectedElementId } ] ,
242+ description : `Removed ${ describeElement ( spec , selectedElementId ) } .` ,
243+ } ;
244+ }
221245 if ( ! matched ) return { operations : [ ] , description : 'Could not determine which element to remove.' } ;
222246
223- const all = prompt . includes ( 'all' ) ;
224- const matches = findByType ( spec , matched ) ;
247+ const all = normalizedPrompt . includes ( 'all' ) ;
248+ const scopedMatches = selectedElementId ? findByTypeInScope ( spec , matched , selectedElementId ) : [ ] ;
249+ const matches = scopedMatches . length > 0 ? scopedMatches : findByType ( spec , matched ) ;
225250 if ( matches . length === 0 ) return { operations : [ ] , description : `No ${ matched } elements found.` } ;
226251
227252 const targets = all ? matches : [ matches [ 0 ] ] ;
@@ -231,19 +256,54 @@ export class MockProvider implements AIProvider {
231256 } ;
232257 }
233258
234- private handleUpdate ( prompt : string , spec : DesignSpec ) : AIResponse {
259+ private handleUpdate (
260+ prompt : string ,
261+ spec : DesignSpec ,
262+ selectedElementId : string | null ,
263+ validationProvider ?: string ,
264+ ) : AIResponse {
265+ const normalizedPrompt = prompt . toLowerCase ( ) ;
235266 const componentTypes = Object . keys ( catalog ) as ComponentType [ ] ;
236- const matched = componentTypes . find ( ( t ) => prompt . includes ( t . toLowerCase ( ) ) ) ;
237- const targets = matched ? findByType ( spec , matched ) : Object . entries ( spec . elements ) . filter ( ( [ id ] ) => id !== spec . root ) ;
267+ const matched = componentTypes . find ( ( t ) => normalizedPrompt . includes ( t . toLowerCase ( ) ) ) ;
268+ const targets = resolveUpdateTargets ( spec , matched , selectedElementId ) ;
238269 if ( targets . length === 0 ) return { operations : [ ] , description : 'No matching elements found.' } ;
239270
240271 const [ targetId ] = targets [ 0 ] ;
241272 const updatedProps : Record < string , unknown > = { } ;
242273 const quoted = extractQuotedText ( prompt ) ;
274+ const requiredIntent = / \b r e q u i r e d \b / . test ( normalizedPrompt ) || / \b r e q u i r e \b / . test ( normalizedPrompt ) ;
275+ if ( requiredIntent ) {
276+ const fieldTargetId = resolveSelectedElementTarget ( spec , selectedElementId ?? targetId ) ?? targetId ;
277+ const operations : AIOperation [ ] = [
278+ {
279+ type : 'updateProps' ,
280+ elementId : fieldTargetId ,
281+ updatedProps : { required : true } ,
282+ } ,
283+ ] ;
284+
285+ const hintTargetId = resolveValidationHintTarget ( spec , selectedElementId ?? targetId ) ;
286+ if ( hintTargetId ) {
287+ operations . push ( {
288+ type : 'updateProps' ,
289+ elementId : hintTargetId ,
290+ updatedProps : {
291+ children : buildRequiredHint ( validationProvider ) ,
292+ } ,
293+ } ) ;
294+ }
295+
296+ return {
297+ operations,
298+ description : buildRequiredDescription ( spec , fieldTargetId , validationProvider ) ,
299+ } ;
300+ }
301+
243302 if ( quoted ) updatedProps . children = quoted ;
244- if ( prompt . includes ( 'destructive' ) ) updatedProps . variant = 'destructive' ;
245- if ( prompt . includes ( 'outline' ) ) updatedProps . variant = 'outline' ;
246- if ( prompt . includes ( 'disabled' ) ) updatedProps . disabled = true ;
303+ if ( normalizedPrompt . includes ( 'destructive' ) ) updatedProps . variant = 'destructive' ;
304+ if ( normalizedPrompt . includes ( 'outline' ) ) updatedProps . variant = 'outline' ;
305+ if ( normalizedPrompt . includes ( 'disabled' ) ) updatedProps . disabled = true ;
306+ if ( normalizedPrompt . includes ( 'enabled' ) ) updatedProps . disabled = false ;
247307
248308 if ( Object . keys ( updatedProps ) . length === 0 ) {
249309 return { operations : [ ] , description : 'Could not determine what to change.' } ;
@@ -741,6 +801,66 @@ function delay(ms: number): Promise<void> {
741801 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
742802}
743803
804+ function resolveUpdateTargets (
805+ spec : DesignSpec ,
806+ matchedType : ComponentType | undefined ,
807+ selectedElementId : string | null ,
808+ ) : [ string , Element ] [ ] {
809+ if ( selectedElementId ) {
810+ if ( matchedType ) {
811+ const scopedMatches = findByTypeInScope ( spec , matchedType , selectedElementId ) ;
812+ if ( scopedMatches . length > 0 ) return scopedMatches ;
813+ }
814+
815+ const selectedTargetId = resolveSelectedElementTarget ( spec , selectedElementId ) ?? selectedElementId ;
816+ const selectedTarget = spec . elements [ selectedTargetId ] ;
817+ if ( selectedTarget ) return [ [ selectedTargetId , selectedTarget ] ] ;
818+ }
819+
820+ return matchedType
821+ ? findByType ( spec , matchedType )
822+ : Object . entries ( spec . elements ) . filter ( ( [ id ] ) => id !== spec . root ) ;
823+ }
824+
825+ function findByTypeInScope ( spec : DesignSpec , type : string , rootId : string ) : [ string , Element ] [ ] {
826+ const scopeIds = new Set ( collectSubtreeElementIds ( spec , rootId ) ) ;
827+ return findByType ( spec , type ) . filter ( ( [ id ] ) => scopeIds . has ( id ) ) ;
828+ }
829+
830+ function describeElement ( spec : DesignSpec , elementId : string ) : string {
831+ const element = spec . elements [ elementId ] ;
832+ if ( ! element ) return 'element' ;
833+ return element . __editor ?. name ?? element . type ;
834+ }
835+
836+ function buildRequiredHint ( validationProvider ?: string ) : string {
837+ if ( validationProvider === 'react-hook-form' ) {
838+ return 'Required. Enforce this with React Hook Form rules and surface field errors before submit.' ;
839+ }
840+
841+ if ( validationProvider === 'tanstack-form' ) {
842+ return 'Required. Enforce this with TanStack Form validators before submit.' ;
843+ }
844+
845+ return 'Required. Validate before submit.' ;
846+ }
847+
848+ function buildRequiredDescription (
849+ spec : DesignSpec ,
850+ elementId : string ,
851+ validationProvider ?: string ,
852+ ) : string {
853+ const providerLabel =
854+ validationProvider === 'react-hook-form'
855+ ? 'React Hook Form'
856+ : validationProvider === 'tanstack-form'
857+ ? 'TanStack Form'
858+ : null ;
859+ return providerLabel
860+ ? `Marked ${ describeElement ( spec , elementId ) } as required using ${ providerLabel } guidance.`
861+ : `Marked ${ describeElement ( spec , elementId ) } as required.` ;
862+ }
863+
744864function extractQuotedText ( input : string ) : string | null {
745865 const match = input . match ( / [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / ) ;
746866 return match ? match [ 1 ] : null ;
0 commit comments