11// deno-lint-ignore-file no-explicit-any
22import { DreamlabEditorUIComponent } from "./_component.tsx" ;
33import { ClientGame } from "@dreamlab/engine" ;
4- import { BehaviorSchema } from "@dreamlab/scene" ;
5- import { EditorMetadataEntity } from "../../common/mod.ts" ;
4+ import { connectionDetails } from "@dreamlab/client/util/server-url.ts" ;
65import {
76 addBehavior ,
87 lookupEntityInEditMode ,
98 spawnEntity ,
109} from "./assistant/editor-world-interaction-util.ts" ;
10+ import { icon , Minimize2 , Check , X as XIcon } from "../_icons.tsx" ;
1111
1212type Action = {
1313 id : number ;
@@ -16,34 +16,34 @@ type Action = {
1616 code : any ; // Store the full plan item
1717} ;
1818
19- const code = `
20- const prefabRoot = lookupById("prefabs");
21-
22- // Create Enemy prefab - a CharacterController with ColoredSquare child and enemy behavior
23- spawnEntity(prefabRoot, {
24- name: "Enemy",
25- type: "CharacterController",
26- behaviors: [{script: "res://src/enemy.ts"}],
27- transform: { scale: { x: 0.8, y: 0.8 } },
28- children: [{
29- type: "ColoredSquare",
30- name: "ColoredSquare",
31- transform: { scale: { x: 1, y: 1 } }
32- }]
33- });
34-
35- // Create Enemy Spawner in the world
36- const worldRoot = lookupById("world");
37- const newE = spawnEntity(worldRoot, {
38- name: "EnemySpawner",
39- type: "Empty",
40- // behaviors: [{script: "res://src/enemy-spawner.ts"}],
41- transform: { position: { x: 10, y: 5 } }
42- });
43-
44- addBehavior(newE, "src/enemy-spawner.ts");
45- addBehavior(newE, "src/camera-follow.ts", {smoothFactor: 69});
46- ` ;
19+ // const code = `
20+ // const prefabRoot = lookupById("prefabs");
21+
22+ // // Create Enemy prefab - a CharacterController with ColoredSquare child and enemy behavior
23+ // spawnEntity(prefabRoot, {
24+ // name: "Enemy",
25+ // type: "CharacterController",
26+ // behaviors: [{script: "res://src/enemy.ts"}],
27+ // transform: { scale: { x: 0.8, y: 0.8 } },
28+ // children: [{
29+ // type: "ColoredSquare",
30+ // name: "ColoredSquare",
31+ // transform: { scale: { x: 1, y: 1 } }
32+ // }]
33+ // });
34+
35+ // // Create Enemy Spawner in the world
36+ // const worldRoot = lookupById("world");
37+ // const newE = spawnEntity(worldRoot, {
38+ // name: "EnemySpawner",
39+ // type: "Empty",
40+ // // behaviors: [{script: "res://src/enemy-spawner.ts"}],
41+ // transform: { position: { x: 10, y: 5 } }
42+ // });
43+
44+ // addBehavior(newE, "src/enemy-spawner.ts");
45+ // addBehavior(newE, "src/camera-follow.ts", {smoothFactor: 69});
46+ // `;
4747
4848export class AISuggestionsPopup extends DreamlabEditorUIComponent {
4949 state = {
@@ -61,10 +61,8 @@ export class AISuggestionsPopup extends DreamlabEditorUIComponent {
6161
6262 globalThis . addEventListener ( "message" , message => {
6363 if ( ! message . data . payload ) return ;
64- console . debug ( message ) ;
6564 if ( message . data . payload ?. length > 0 ) {
6665 this . setPlan ( message . data . payload ) ;
67- console . log ( "showing" ) ;
6866 this . show ( ) ;
6967 }
7068 // this contains array of {editDescription: "title", editCode: "code to be run"}
@@ -90,12 +88,26 @@ export class AISuggestionsPopup extends DreamlabEditorUIComponent {
9088 this . rerender ( ) ;
9189 } ;
9290
93- /**
94- * Handles the application of an action when the user clicks Apply
95- */
91+ deleteEditorActionsFile = async ( ) => {
92+ try {
93+ const url = new URL ( connectionDetails . serverUrl ) ;
94+ url . pathname = `/api/v1/edit/${ this . game . worldId } /editor-actions` ;
95+ const response = await fetch ( url , {
96+ method : "DELETE" ,
97+ } ) ;
98+ if ( ! response . ok ) {
99+ console . error ( "Failed to delete editorActions.json" ) ;
100+ }
101+ } catch ( error ) {
102+ console . error ( "Error deleting editorActions.json:" , error ) ;
103+ }
104+ } ;
105+
96106 handleApply = async ( id : number ) => {
97107 const action = this . state . actions . find ( a => a . id === id ) ;
98- if ( ! action || action . applied ) return ;
108+ if ( ! action || action . applied ) {
109+ return ;
110+ }
99111
100112 try {
101113 new Function ( "spawnEntity" , "lookupById" , "addBehavior" , action . code ) (
@@ -104,99 +116,113 @@ export class AISuggestionsPopup extends DreamlabEditorUIComponent {
104116 addBehavior ,
105117 ) ;
106118
107- // Mark the action as applied
108119 this . state . actions = this . state . actions . map ( action =>
109120 action . id === id ? { ...action , applied : true } : action ,
110121 ) ;
111122 this . rerender ( ) ;
112123
113- // Check if all actions are applied and auto-close if so
114124 const allApplied = this . state . actions . every ( action => action . applied ) ;
115125 if ( allApplied ) {
126+ await this . deleteEditorActionsFile ( ) ;
116127 this . handleClose ( ) ;
128+ const tab = document . getElementById ( "recommendedActionsTab" ) ;
129+ if ( tab ) tab . classList . add ( "hidden" ) ;
117130 }
118131 } catch ( error ) {
119132 console . error ( "Error applying action:" , error ) ;
120- // Optionally show an error message to the user
121133 }
122134 } ;
123- /**
124- * Closes the popup
125- */
135+
136+ handleDismissAction = ( id : number ) => {
137+ this . state . actions = this . state . actions . filter ( action => action . id !== id ) ;
138+ this . rerender ( ) ;
139+
140+ if ( this . state . actions . length === 0 ) {
141+ this . handleDismiss ( ) ;
142+ }
143+ } ;
144+
145+ handleDismiss = async ( ) => {
146+ await this . deleteEditorActionsFile ( ) ;
147+ this . hide ( ) ;
148+ const tab = document . getElementById ( "recommendedActionsTab" ) ;
149+ if ( tab ) tab . classList . add ( "hidden" ) ;
150+ } ;
151+
126152 handleClose = ( ) => {
127153 this . hide ( ) ;
128154 } ;
129155
130156 render ( ) {
157+ const completedCount = this . state . actions . filter ( a => a . applied ) . length ;
158+ const totalCount = this . state . actions . length ;
159+
131160 return (
132- < div
133- className = "ai-actions-menu"
134- style = { { width : "450px" , height : "400px" , zIndex : "2000" } }
135- >
136- { /* Close button */ }
137- < button
138- onClick = { this . handleClose }
139- style = { {
140- position : "absolute" ,
141- top : "10px" ,
142- right : "10px" ,
143- background : "transparent" ,
144- border : "none" ,
145- fontSize : "18px" ,
146- cursor : "pointer" ,
147- } }
148- >
149- ✕
150- </ button >
151-
152- < h2 style = { { marginBottom : "20px" } } > Recommended Actions from Assistant</ h2 >
153-
154- < ul style = { { listStyle : "none" , padding : "0" } } >
155- { this . state . actions . map ( action => (
156- < li
157- style = { {
158- display : "flex" ,
159- alignItems : "center" ,
160- justifyContent : "space-between" ,
161- padding : "12px 16px" ,
162- margin : "8px 0" ,
163- borderRadius : "4px" ,
164- backgroundColor : "#cfcfcf" ,
165- boxShadow : "0 1px 3px rgba(0,0,0,0.1)" ,
166- } }
167- >
168- < span
169- style = { {
170- textDecoration : action . applied ? "line-through" : "none" ,
171- color : action . applied ? "#777" : "#000" ,
172- marginRight : "12px" ,
173- flex : "1" ,
174- } }
175- >
176- { action . text }
177- </ span >
178-
179- < button
180- onClick = { ( ) => ! action . applied && this . handleApply ( action . id ) }
181- style = { {
182- padding : "6px 14px" ,
183- background : action . applied ? "#cccccc" : "#28a745" ,
184- color : "#fff" ,
185- border : "none" ,
186- borderRadius : "4px" ,
187- cursor : action . applied ? "default" : "pointer" ,
188- transition : "all 0.2s ease" ,
189- fontWeight : "500" ,
190- boxShadow : action . applied ? "none" : "0 2px 4px rgba(0,0,0,0.1)" ,
191- opacity : action . applied ? "0.8" : "1" ,
192- minWidth : "80px" ,
193- } }
194- >
195- { action . applied ? "Applied" : "Apply" }
196- </ button >
197- </ li >
198- ) ) }
199- </ ul >
161+ < div className = "ai-actions-popup" >
162+ < div className = "popup-header" >
163+ < div style = { { display : "flex" , alignItems : "center" , gap : "12px" , flex : 1 } } >
164+ < h1 > AI Suggested Actions</ h1 >
165+ < span className = "progress-text" style = { { fontSize : "12px" , opacity : 0.7 } } >
166+ { completedCount } / { totalCount }
167+ </ span >
168+ </ div >
169+ < button type = "button" className = "dismiss-button" onClick = { this . handleDismiss } >
170+ { icon ( XIcon ) } Dismiss All
171+ </ button >
172+ < button type = "button" className = "close-button" onClick = { this . handleClose } >
173+ { icon ( Minimize2 ) }
174+ </ button >
175+ </ div >
176+
177+ < div className = "popup-content" >
178+ { completedCount > 0 && completedCount < totalCount && (
179+ < div className = "progress-bar" >
180+ < div
181+ className = "progress-fill"
182+ style = { { width : `${ ( completedCount / totalCount ) * 100 } %` } }
183+ />
184+ </ div >
185+ ) }
186+
187+ < div className = "actions-list" >
188+ { this . state . actions
189+ . filter ( a => a && a . text )
190+ . map ( action => (
191+ < div className = { `action-item ${ action . applied ? "applied" : "" } ` } >
192+ < div className = "action-content" >
193+ < div className = "action-icon" >
194+ { action . applied ? (
195+ icon ( Check )
196+ ) : (
197+ < span className = "action-number" > { action . id } </ span >
198+ ) }
199+ </ div >
200+ < span className = "action-text" > { action . text } </ span >
201+ </ div >
202+ < div className = "action-buttons" >
203+ { ! action . applied && (
204+ < button
205+ type = "button"
206+ className = "action-dismiss-button"
207+ onClick = { ( ) => this . handleDismissAction ( action . id ) }
208+ title = "Skip this action"
209+ >
210+ { icon ( XIcon ) }
211+ </ button >
212+ ) }
213+ < button
214+ type = "button"
215+ className = { `action-button ${ action . applied ? "applied" : "" } ` }
216+ onClick = { ( ) => ! action . applied && this . handleApply ( action . id ) }
217+ disabled = { action . applied }
218+ >
219+ { action . applied ? "Applied" : "Apply" }
220+ </ button >
221+ </ div >
222+ </ div >
223+ ) ) }
224+ </ div >
225+ </ div >
200226 </ div >
201227 ) ;
202228 }
0 commit comments