Skip to content

Commit 97d85bf

Browse files
committed
allow external file modifiers execute editor actions
1 parent bf3e54a commit 97d85bf

8 files changed

Lines changed: 500 additions & 160 deletions

File tree

editor/client/ui/ai-suggestions-popup.tsx

Lines changed: 136 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// deno-lint-ignore-file no-explicit-any
22
import { DreamlabEditorUIComponent } from "./_component.tsx";
33
import { 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";
65
import {
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

1212
type 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

4848
export 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
}

editor/client/ui/bottom-tabs.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export class BottomTabs implements InspectorUIWidget {
112112
aiSuggestionsPopup.show();
113113
e.preventDefault();
114114
e.stopPropagation();
115-
recommendedActionsTab.classList.add("hidden");
116115
});
117116

118117
// @ts-expect-error Global

0 commit comments

Comments
 (0)