aecode 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aecode/__init__.py +7 -0
- aecode/api_engine.py +161 -0
- aecode/cli.py +61 -0
- aecode/flow_engine.py +138 -0
- aecode/ui_engine.py +661 -0
- aecode/web_engine.py +85 -0
- aecode-0.2.0.dist-info/METADATA +214 -0
- aecode-0.2.0.dist-info/RECORD +12 -0
- aecode-0.2.0.dist-info/WHEEL +5 -0
- aecode-0.2.0.dist-info/entry_points.txt +2 -0
- aecode-0.2.0.dist-info/licenses/LICENSE +21 -0
- aecode-0.2.0.dist-info/top_level.txt +1 -0
aecode/ui_engine.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import webbrowser
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from lark import Lark, Transformer, v_args
|
|
6
|
+
|
|
7
|
+
AEUI_GRAMMAR = r'''
|
|
8
|
+
start: statement*
|
|
9
|
+
|
|
10
|
+
?statement: page_stmt
|
|
11
|
+
| nav_stmt
|
|
12
|
+
| hero_stmt
|
|
13
|
+
| grid_stmt
|
|
14
|
+
| btn_stmt
|
|
15
|
+
| testimonials_stmt
|
|
16
|
+
| form_stmt
|
|
17
|
+
| canvas_stmt
|
|
18
|
+
| palette_stmt
|
|
19
|
+
| sizes_stmt
|
|
20
|
+
| tools_stmt
|
|
21
|
+
| footer_stmt
|
|
22
|
+
| state_stmt
|
|
23
|
+
| comment
|
|
24
|
+
|
|
25
|
+
page_stmt: "PAGE" (PATH | IDENT)
|
|
26
|
+
state_stmt: "STATE" /.+/
|
|
27
|
+
nav_stmt: "NAV" "[" [nav_item ("," nav_item)*] "]"
|
|
28
|
+
?nav_item: links_call | nav_text
|
|
29
|
+
links_call: "Links(" [LINK_ITEM ("," LINK_ITEM)*] ")"
|
|
30
|
+
nav_text: /[^,\]\r\n]+/
|
|
31
|
+
|
|
32
|
+
hero_stmt: "HERO" ESCAPED_STRING [ALIGN]
|
|
33
|
+
ALIGN: "center" | "left" | "right"
|
|
34
|
+
|
|
35
|
+
grid_stmt: "GRID" INT (">" grid_target | block)
|
|
36
|
+
grid_target: card_call | statement | block
|
|
37
|
+
block: "{" statement* "}" | "[" statement* "]"
|
|
38
|
+
card_call: "CARD" "(" [FIELD ("," FIELD)*] ")"
|
|
39
|
+
|
|
40
|
+
btn_stmt: "BTN" ESCAPED_STRING
|
|
41
|
+
testimonials_stmt: "TESTIMONIALS"
|
|
42
|
+
form_stmt: "FORM" ESCAPED_STRING
|
|
43
|
+
footer_stmt: "FOOTER"
|
|
44
|
+
|
|
45
|
+
canvas_stmt: "CANVAS" DIMENSIONS
|
|
46
|
+
palette_stmt: "PALETTE" "[" [COLOR ("," COLOR)*] "]"
|
|
47
|
+
sizes_stmt: "SIZES" "[" [NUMBER ("," NUMBER)*] "]"
|
|
48
|
+
tools_stmt: "TOOLS" "[" [TOOL ("," TOOL)*] "]"
|
|
49
|
+
|
|
50
|
+
DIMENSIONS: /[0-9]+x[0-9]+/
|
|
51
|
+
COLOR: /#[0-9a-fA-F]{3,8}/
|
|
52
|
+
TOOL: /[a-zA-Z]+/
|
|
53
|
+
FIELD: /[a-zA-Z0-9_]+/
|
|
54
|
+
LINK_ITEM: /[^,\)\r\n]+/
|
|
55
|
+
PATH: /\/[a-zA-Z0-9_\-\/]*/
|
|
56
|
+
IDENT: /[a-zA-Z0-9_\-]+/
|
|
57
|
+
|
|
58
|
+
comment: /\/\/.*/
|
|
59
|
+
|
|
60
|
+
%import common.ESCAPED_STRING
|
|
61
|
+
%import common.INT
|
|
62
|
+
%import common.NUMBER
|
|
63
|
+
%import common.WS
|
|
64
|
+
%ignore WS
|
|
65
|
+
'''
|
|
66
|
+
|
|
67
|
+
_aeui_parser = None
|
|
68
|
+
def get_parser():
|
|
69
|
+
global _aeui_parser
|
|
70
|
+
if _aeui_parser is None:
|
|
71
|
+
_aeui_parser = Lark(AEUI_GRAMMAR, parser='earley')
|
|
72
|
+
return _aeui_parser
|
|
73
|
+
|
|
74
|
+
class AeUITransformer(Transformer):
|
|
75
|
+
def __init__(self):
|
|
76
|
+
super().__init__()
|
|
77
|
+
self.title = "AeUI Page"
|
|
78
|
+
self.has_canvas = False
|
|
79
|
+
|
|
80
|
+
def page_stmt(self, children):
|
|
81
|
+
val = str(children[0]).strip()
|
|
82
|
+
self.title = val
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
def state_stmt(self, children):
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
def comment(self, children):
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def nav_stmt(self, children):
|
|
92
|
+
items = children
|
|
93
|
+
nav_html = '<nav class="bg-gray-900 text-white px-8 py-4 flex justify-between items-center shadow-md">\n'
|
|
94
|
+
for item in items:
|
|
95
|
+
if isinstance(item, dict) and item.get('type') == 'links':
|
|
96
|
+
nav_html += ' <div class="flex space-x-6">\n'
|
|
97
|
+
for link in item['links']:
|
|
98
|
+
nav_html += f' <a href="#" class="text-gray-300 hover:text-amber-400 transition">{link}</a>\n'
|
|
99
|
+
nav_html += ' </div>\n'
|
|
100
|
+
else:
|
|
101
|
+
text = str(item).strip()
|
|
102
|
+
if text:
|
|
103
|
+
nav_html += f' <div class="font-extrabold text-2xl text-amber-500">{text}</div>\n'
|
|
104
|
+
nav_html += '</nav>\n'
|
|
105
|
+
return nav_html
|
|
106
|
+
|
|
107
|
+
def links_call(self, children):
|
|
108
|
+
links = [str(c).strip() for c in children if str(c).strip()]
|
|
109
|
+
return {'type': 'links', 'links': links}
|
|
110
|
+
|
|
111
|
+
def nav_text(self, children):
|
|
112
|
+
return str(children[0]).strip()
|
|
113
|
+
|
|
114
|
+
def hero_stmt(self, children):
|
|
115
|
+
raw_text = children[0]
|
|
116
|
+
text = json.loads(raw_text) if raw_text.startswith('"') else str(raw_text)
|
|
117
|
+
align = "text-center"
|
|
118
|
+
if len(children) > 1 and children[1]:
|
|
119
|
+
align_val = str(children[1]).lower()
|
|
120
|
+
align = "text-center" if "center" in align_val else "text-left"
|
|
121
|
+
return f'''<header class="bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900 text-white py-24 px-6 {align}">
|
|
122
|
+
<div class="max-w-4xl mx-auto">
|
|
123
|
+
<h1 class="text-5xl md:text-6xl font-black tracking-tight mb-6">{text}</h1>
|
|
124
|
+
</div>
|
|
125
|
+
</header>'''
|
|
126
|
+
|
|
127
|
+
def block(self, children):
|
|
128
|
+
# Flatten and filter out empty strings
|
|
129
|
+
items = [c for c in children if c and isinstance(c, str)]
|
|
130
|
+
return items
|
|
131
|
+
|
|
132
|
+
def card_call(self, children):
|
|
133
|
+
fields = [str(c).strip() for c in children]
|
|
134
|
+
return {'type': 'cards', 'fields': fields}
|
|
135
|
+
|
|
136
|
+
def grid_target(self, children):
|
|
137
|
+
return children[0]
|
|
138
|
+
|
|
139
|
+
def grid_stmt(self, children):
|
|
140
|
+
cols = int(children[0])
|
|
141
|
+
target = children[1] if len(children) > 1 else None
|
|
142
|
+
|
|
143
|
+
grid_html = f'<section class="max-w-6xl mx-auto px-6 py-16">\n <div class="grid grid-cols-1 md:grid-cols-{cols} gap-8">\n'
|
|
144
|
+
|
|
145
|
+
if isinstance(target, dict) and target.get('type') == 'cards':
|
|
146
|
+
# Generate placeholder cards
|
|
147
|
+
for i in range(cols):
|
|
148
|
+
grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 hover:border-amber-500/50 p-6 shadow-xl transition-all hover:-translate-y-2">
|
|
149
|
+
<div class="h-44 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4"></div>
|
|
150
|
+
<h3 class="text-xl font-bold text-white mb-2">Card {i+1}</h3>
|
|
151
|
+
<p class="text-gray-400 mb-4">Descripcion</p>
|
|
152
|
+
<button class="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white font-semibold rounded-lg">Action</button>
|
|
153
|
+
</div>\n'''
|
|
154
|
+
elif isinstance(target, list):
|
|
155
|
+
# Nested statements inside block [ stmt1 stmt2 ] or { stmt1 stmt2 }
|
|
156
|
+
for item in target:
|
|
157
|
+
grid_html += f' <div class="flex flex-col justify-center items-center w-full">{item}</div>\n'
|
|
158
|
+
elif isinstance(target, str) and target:
|
|
159
|
+
# Single statement nested
|
|
160
|
+
grid_html += f' <div class="flex flex-col justify-center items-center w-full">{target}</div>\n'
|
|
161
|
+
else:
|
|
162
|
+
for i in range(cols):
|
|
163
|
+
grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 p-6 shadow-xl">
|
|
164
|
+
<h3 class="text-xl font-bold text-white mb-2">Item {i+1}</h3>
|
|
165
|
+
</div>\n'''
|
|
166
|
+
|
|
167
|
+
grid_html += ' </div>\n</section>\n'
|
|
168
|
+
return grid_html
|
|
169
|
+
|
|
170
|
+
def btn_stmt(self, children):
|
|
171
|
+
raw = children[0]
|
|
172
|
+
label = json.loads(raw) if raw.startswith('"') else str(raw)
|
|
173
|
+
return f'''<div class="text-center py-8">
|
|
174
|
+
<button class="px-10 py-4 bg-gradient-to-r from-amber-500 to-orange-600 text-gray-950 font-black text-xl rounded-2xl shadow-2xl hover:scale-105 transition">{label}</button>
|
|
175
|
+
</div>'''
|
|
176
|
+
|
|
177
|
+
def testimonials_stmt(self, children):
|
|
178
|
+
return '''<section class="max-w-6xl mx-auto px-6 py-12">
|
|
179
|
+
<h2 class="text-3xl font-bold text-center text-white mb-8">Lo que dicen nuestros usuarios</h2>
|
|
180
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
181
|
+
<div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
|
|
182
|
+
<p class="text-gray-300 italic mb-4">"AeCode redujo nuestros costos de tokens de IA en más de 80%. Esencial para cualquier agente."</p>
|
|
183
|
+
<span class="font-bold text-amber-400">— Sarah C., AI Lead</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
|
|
186
|
+
<p class="text-gray-300 italic mb-4">"Construir scrapers y dashboards en 3 líneas de código parece magia. 10/10."</p>
|
|
187
|
+
<span class="font-bold text-amber-400">— David R., Senior Engineer</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</section>'''
|
|
191
|
+
|
|
192
|
+
def form_stmt(self, children):
|
|
193
|
+
raw = children[0]
|
|
194
|
+
form_title = json.loads(raw) if raw.startswith('"') else str(raw)
|
|
195
|
+
return f'''<section class="max-w-xl mx-auto px-6 py-10">
|
|
196
|
+
<div class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-xl text-center">
|
|
197
|
+
<h3 class="text-2xl font-bold text-white mb-4">{form_title}</h3>
|
|
198
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
199
|
+
<input type="email" placeholder="tu@email.com" class="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-amber-500">
|
|
200
|
+
<button class="px-6 py-3 bg-amber-500 hover:bg-amber-400 text-gray-950 font-bold rounded-xl transition">Enviar</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</section>'''
|
|
204
|
+
|
|
205
|
+
def canvas_stmt(self, children):
|
|
206
|
+
self.has_canvas = True
|
|
207
|
+
dims = str(children[0]).split('x')
|
|
208
|
+
w = dims[0] if len(dims) > 0 else "900"
|
|
209
|
+
h = dims[1] if len(dims) > 1 else "550"
|
|
210
|
+
return f'''<div class="max-w-6xl mx-auto px-4 pt-6 pb-2 flex justify-center">
|
|
211
|
+
<canvas id="paintCanvas" width="{w}" height="{h}" class="bg-white rounded-2xl shadow-2xl border-4 border-gray-800 cursor-crosshair touch-none max-w-full block"></canvas>
|
|
212
|
+
</div>'''
|
|
213
|
+
|
|
214
|
+
def palette_stmt(self, children):
|
|
215
|
+
self.has_canvas = True
|
|
216
|
+
colors = [str(c).strip() for c in children]
|
|
217
|
+
swatches = []
|
|
218
|
+
for i, c in enumerate(colors):
|
|
219
|
+
active = "ring-4 ring-amber-400 scale-110" if i == 0 else ""
|
|
220
|
+
swatches.append(f'<button type="button" class="palette-swatch w-9 h-9 rounded-full border-2 border-gray-700 shadow-md transition-all duration-150 hover:scale-110 focus:outline-none {active}" data-color="{c}" style="background-color: {c};" title="{c}"></button>')
|
|
221
|
+
return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
222
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Color:</span>
|
|
223
|
+
<div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
224
|
+
{''.join(swatches)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>'''
|
|
227
|
+
|
|
228
|
+
def sizes_stmt(self, children):
|
|
229
|
+
self.has_canvas = True
|
|
230
|
+
sizes = [str(c).strip() for c in children]
|
|
231
|
+
size_btns = []
|
|
232
|
+
for i, s in enumerate(sizes):
|
|
233
|
+
active = "bg-amber-500 text-gray-950 font-bold ring-2 ring-amber-400" if i == 1 or (len(sizes) == 1 and i == 0) else "bg-gray-800 text-gray-300 hover:bg-gray-700 font-semibold"
|
|
234
|
+
size_btns.append(f'<button type="button" class="size-btn px-4 py-2 rounded-xl text-sm transition-all duration-150 border border-gray-700 focus:outline-none {active}" data-size="{s}">{s}px</button>')
|
|
235
|
+
return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
236
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Grosor:</span>
|
|
237
|
+
<div class="flex flex-wrap items-center gap-2 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
238
|
+
{''.join(size_btns)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>'''
|
|
241
|
+
|
|
242
|
+
def tools_stmt(self, children):
|
|
243
|
+
self.has_canvas = True
|
|
244
|
+
tools = [str(c).strip().lower() for c in children]
|
|
245
|
+
tool_icons = {
|
|
246
|
+
"brush": "✏️ Pincel",
|
|
247
|
+
"eraser": "🧹 Borrador",
|
|
248
|
+
"clear": "🗑️ Limpiar",
|
|
249
|
+
"download": "💾 Descargar"
|
|
250
|
+
}
|
|
251
|
+
tool_btns = []
|
|
252
|
+
for t in tools:
|
|
253
|
+
label = tool_icons.get(t, t.capitalize())
|
|
254
|
+
active = "bg-amber-500 text-gray-950 ring-2 ring-amber-400" if t == "brush" else "bg-gray-800 text-gray-200 hover:bg-gray-700"
|
|
255
|
+
tool_btns.append(f'<button type="button" class="tool-btn px-4 py-2 rounded-xl text-sm font-bold transition-all duration-150 border border-gray-700 focus:outline-none flex items-center gap-2 {active}" data-tool="{t}">{label}</button>')
|
|
256
|
+
return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
257
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Herramientas:</span>
|
|
258
|
+
<div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
259
|
+
{''.join(tool_btns)}
|
|
260
|
+
</div>
|
|
261
|
+
</div>'''
|
|
262
|
+
|
|
263
|
+
def footer_stmt(self, children):
|
|
264
|
+
return '''<footer class="bg-gray-900 text-white py-8 px-6 text-center border-t border-gray-800">
|
|
265
|
+
<p class="text-gray-400">Generado con AeCode</p>
|
|
266
|
+
</footer>'''
|
|
267
|
+
|
|
268
|
+
def start(self, children):
|
|
269
|
+
# Collect all non-empty HTML parts
|
|
270
|
+
return [c for c in children if c and isinstance(c, str)]
|
|
271
|
+
|
|
272
|
+
def _get_canvas_script():
|
|
273
|
+
return """<script>
|
|
274
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
275
|
+
const canvas = document.getElementById('paintCanvas');
|
|
276
|
+
if (!canvas) return;
|
|
277
|
+
|
|
278
|
+
const ctx = canvas.getContext('2d');
|
|
279
|
+
const backgroundColor = '#ffffff';
|
|
280
|
+
|
|
281
|
+
function fillBackground() {
|
|
282
|
+
ctx.fillStyle = backgroundColor;
|
|
283
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
284
|
+
}
|
|
285
|
+
fillBackground();
|
|
286
|
+
|
|
287
|
+
let isDrawing = false;
|
|
288
|
+
let lastX = 0;
|
|
289
|
+
let lastY = 0;
|
|
290
|
+
let currentColor = '#000000';
|
|
291
|
+
let currentSize = 5;
|
|
292
|
+
let currentTool = 'brush';
|
|
293
|
+
|
|
294
|
+
function getCoords(e) {
|
|
295
|
+
const rect = canvas.getBoundingClientRect();
|
|
296
|
+
const scaleX = canvas.width / rect.width;
|
|
297
|
+
const scaleY = canvas.height / rect.height;
|
|
298
|
+
let clientX, clientY;
|
|
299
|
+
if (e.touches && e.touches.length > 0) {
|
|
300
|
+
clientX = e.touches[0].clientX;
|
|
301
|
+
clientY = e.touches[0].clientY;
|
|
302
|
+
} else {
|
|
303
|
+
clientX = e.clientX;
|
|
304
|
+
clientY = e.clientY;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
x: (clientX - rect.left) * scaleX,
|
|
308
|
+
y: (clientY - rect.top) * scaleY
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function startDrawing(e) {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
isDrawing = true;
|
|
315
|
+
const pos = getCoords(e);
|
|
316
|
+
lastX = pos.x;
|
|
317
|
+
lastY = pos.y;
|
|
318
|
+
draw(e);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function draw(e) {
|
|
322
|
+
if (!isDrawing) return;
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
const pos = getCoords(e);
|
|
325
|
+
|
|
326
|
+
ctx.beginPath();
|
|
327
|
+
ctx.lineCap = 'round';
|
|
328
|
+
ctx.lineJoin = 'round';
|
|
329
|
+
ctx.lineWidth = currentSize;
|
|
330
|
+
ctx.strokeStyle = (currentTool === 'eraser') ? backgroundColor : currentColor;
|
|
331
|
+
|
|
332
|
+
ctx.moveTo(lastX, lastY);
|
|
333
|
+
ctx.lineTo(pos.x, pos.y);
|
|
334
|
+
ctx.stroke();
|
|
335
|
+
|
|
336
|
+
lastX = pos.x;
|
|
337
|
+
lastY = pos.y;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function stopDrawing(e) {
|
|
341
|
+
if (!isDrawing) return;
|
|
342
|
+
isDrawing = false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
canvas.addEventListener('mousedown', startDrawing);
|
|
346
|
+
canvas.addEventListener('mousemove', draw);
|
|
347
|
+
canvas.addEventListener('mouseup', stopDrawing);
|
|
348
|
+
canvas.addEventListener('mouseleave', stopDrawing);
|
|
349
|
+
|
|
350
|
+
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
351
|
+
canvas.addEventListener('touchmove', draw, { passive: false });
|
|
352
|
+
canvas.addEventListener('touchend', stopDrawing, { passive: false });
|
|
353
|
+
canvas.addEventListener('touchcancel', stopDrawing, { passive: false });
|
|
354
|
+
|
|
355
|
+
// Swatches
|
|
356
|
+
const swatches = document.querySelectorAll('.palette-swatch');
|
|
357
|
+
swatches.forEach(swatch => {
|
|
358
|
+
swatch.addEventListener('click', () => {
|
|
359
|
+
currentColor = swatch.getAttribute('data-color');
|
|
360
|
+
if (currentTool === 'eraser') {
|
|
361
|
+
selectTool('brush');
|
|
362
|
+
}
|
|
363
|
+
swatches.forEach(s => s.classList.remove('ring-4', 'ring-amber-400', 'scale-110'));
|
|
364
|
+
swatch.classList.add('ring-4', 'ring-amber-400', 'scale-110');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
if (swatches.length > 0) {
|
|
368
|
+
currentColor = swatches[0].getAttribute('data-color') || '#000000';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Sizes
|
|
372
|
+
const sizeBtns = document.querySelectorAll('.size-btn');
|
|
373
|
+
sizeBtns.forEach(btn => {
|
|
374
|
+
btn.addEventListener('click', () => {
|
|
375
|
+
currentSize = parseFloat(btn.getAttribute('data-size'));
|
|
376
|
+
sizeBtns.forEach(b => {
|
|
377
|
+
b.classList.remove('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
|
|
378
|
+
b.classList.add('bg-gray-800', 'text-gray-300');
|
|
379
|
+
});
|
|
380
|
+
btn.classList.remove('bg-gray-800', 'text-gray-300');
|
|
381
|
+
btn.classList.add('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
const initialActiveSize = document.querySelector('.size-btn.bg-amber-500');
|
|
385
|
+
if (initialActiveSize) {
|
|
386
|
+
currentSize = parseFloat(initialActiveSize.getAttribute('data-size')) || 5;
|
|
387
|
+
} else if (sizeBtns.length > 0) {
|
|
388
|
+
currentSize = parseFloat(sizeBtns[0].getAttribute('data-size')) || 5;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Tools
|
|
392
|
+
const toolBtns = document.querySelectorAll('.tool-btn');
|
|
393
|
+
function selectTool(toolName) {
|
|
394
|
+
currentTool = toolName;
|
|
395
|
+
toolBtns.forEach(b => {
|
|
396
|
+
const t = b.getAttribute('data-tool');
|
|
397
|
+
if (t === 'clear' || t === 'download') return;
|
|
398
|
+
if (t === toolName) {
|
|
399
|
+
b.classList.remove('bg-gray-800', 'text-gray-200');
|
|
400
|
+
b.classList.add('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
|
|
401
|
+
} else {
|
|
402
|
+
b.classList.remove('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
|
|
403
|
+
b.classList.add('bg-gray-800', 'text-gray-200');
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
toolBtns.forEach(btn => {
|
|
409
|
+
const tool = btn.getAttribute('data-tool');
|
|
410
|
+
btn.addEventListener('click', () => {
|
|
411
|
+
if (tool === 'brush') {
|
|
412
|
+
selectTool('brush');
|
|
413
|
+
} else if (tool === 'eraser') {
|
|
414
|
+
selectTool('eraser');
|
|
415
|
+
} else if (tool === 'clear') {
|
|
416
|
+
fillBackground();
|
|
417
|
+
} else if (tool === 'download') {
|
|
418
|
+
const link = document.createElement('a');
|
|
419
|
+
link.download = 'aepaint.png';
|
|
420
|
+
link.href = canvas.toDataURL('image/png');
|
|
421
|
+
link.click();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
</script>"""
|
|
427
|
+
|
|
428
|
+
def build_ui(code):
|
|
429
|
+
try:
|
|
430
|
+
parser = get_parser()
|
|
431
|
+
tree = parser.parse(code)
|
|
432
|
+
transformer = AeUITransformer()
|
|
433
|
+
body_parts = transformer.transform(tree)
|
|
434
|
+
title = transformer.title
|
|
435
|
+
has_canvas = transformer.has_canvas
|
|
436
|
+
except Exception as e:
|
|
437
|
+
# Fallback to regex line-by-line if grammar encounters an untracked edge case
|
|
438
|
+
return _build_ui_fallback(code)
|
|
439
|
+
|
|
440
|
+
script_tag = _get_canvas_script() if has_canvas else ""
|
|
441
|
+
html = f"""<!DOCTYPE html>
|
|
442
|
+
<html lang="es">
|
|
443
|
+
<head>
|
|
444
|
+
<meta charset="UTF-8">
|
|
445
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
446
|
+
<title>{title}</title>
|
|
447
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
448
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
|
|
449
|
+
<style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
|
|
450
|
+
</head>
|
|
451
|
+
<body class="bg-gray-950 text-gray-100 min-h-screen pb-12">
|
|
452
|
+
{''.join(body_parts)}
|
|
453
|
+
{script_tag}
|
|
454
|
+
</body>
|
|
455
|
+
</html>"""
|
|
456
|
+
return html
|
|
457
|
+
|
|
458
|
+
def _build_ui_fallback(code):
|
|
459
|
+
title = "AeUI Page"
|
|
460
|
+
body_parts = []
|
|
461
|
+
has_canvas = False
|
|
462
|
+
lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//')]
|
|
463
|
+
|
|
464
|
+
for line in lines:
|
|
465
|
+
if line.startswith('PAGE'):
|
|
466
|
+
parts = line.split()
|
|
467
|
+
if len(parts) > 1:
|
|
468
|
+
title = parts[1]
|
|
469
|
+
elif line.startswith('STATE'):
|
|
470
|
+
pass
|
|
471
|
+
elif line.startswith('NAV'):
|
|
472
|
+
content = extract_brackets(line)
|
|
473
|
+
items = split_items(content)
|
|
474
|
+
nav_html = '<nav class="bg-gray-900 text-white px-8 py-4 flex justify-between items-center shadow-md">\n'
|
|
475
|
+
for item in items:
|
|
476
|
+
if 'Links(' in item:
|
|
477
|
+
links = extract_parens(item)
|
|
478
|
+
link_items = split_items(links)
|
|
479
|
+
nav_html += ' <div class="flex space-x-6">\n'
|
|
480
|
+
for link in link_items:
|
|
481
|
+
nav_html += f' <a href="#" class="text-gray-300 hover:text-amber-400 transition">{link}</a>\n'
|
|
482
|
+
nav_html += ' </div>\n'
|
|
483
|
+
else:
|
|
484
|
+
nav_html += f' <div class="font-extrabold text-2xl text-amber-500">{item}</div>\n'
|
|
485
|
+
nav_html += '</nav>\n'
|
|
486
|
+
body_parts.append(nav_html)
|
|
487
|
+
elif line.startswith('HERO'):
|
|
488
|
+
m = re.search(r'HERO\s+"([^"]+)"(?:\s+(.*))?', line)
|
|
489
|
+
if m:
|
|
490
|
+
text = m.group(1)
|
|
491
|
+
styles = m.group(2) or ""
|
|
492
|
+
align = "text-center" if "center" in styles else "text-left"
|
|
493
|
+
body_parts.append(f'''<header class="bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900 text-white py-24 px-6 {align}">
|
|
494
|
+
<div class="max-w-4xl mx-auto">
|
|
495
|
+
<h1 class="text-5xl md:text-6xl font-black tracking-tight mb-6">{text}</h1>
|
|
496
|
+
</div>
|
|
497
|
+
</header>''')
|
|
498
|
+
elif line.startswith('GRID'):
|
|
499
|
+
m = re.search(r'GRID\s+(\d+)\s*>\s*(.*)', line)
|
|
500
|
+
if m:
|
|
501
|
+
cols = m.group(1)
|
|
502
|
+
grid_html = f'<section class="max-w-6xl mx-auto px-6 py-16">\n <div class="grid grid-cols-1 md:grid-cols-{cols} gap-8">\n'
|
|
503
|
+
for i in range(int(cols)):
|
|
504
|
+
grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 hover:border-amber-500/50 p-6 shadow-xl transition-all hover:-translate-y-2">
|
|
505
|
+
<div class="h-44 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4"></div>
|
|
506
|
+
<h3 class="text-xl font-bold text-white mb-2">Card {i+1}</h3>
|
|
507
|
+
<p class="text-gray-400 mb-4">Descripcion</p>
|
|
508
|
+
<button class="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white font-semibold rounded-lg">Action</button>
|
|
509
|
+
</div>\n'''
|
|
510
|
+
grid_html += ' </div>\n</section>\n'
|
|
511
|
+
body_parts.append(grid_html)
|
|
512
|
+
elif line.startswith('BTN'):
|
|
513
|
+
m = re.search(r'BTN\s+"([^"]+)"', line)
|
|
514
|
+
if m:
|
|
515
|
+
label = m.group(1)
|
|
516
|
+
body_parts.append(f'''<div class="text-center py-8">
|
|
517
|
+
<button class="px-10 py-4 bg-gradient-to-r from-amber-500 to-orange-600 text-gray-950 font-black text-xl rounded-2xl shadow-2xl hover:scale-105 transition">{label}</button>
|
|
518
|
+
</div>''')
|
|
519
|
+
elif line.startswith('TESTIMONIALS'):
|
|
520
|
+
body_parts.append('''<section class="max-w-6xl mx-auto px-6 py-12">
|
|
521
|
+
<h2 class="text-3xl font-bold text-center text-white mb-8">Lo que dicen nuestros usuarios</h2>
|
|
522
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
523
|
+
<div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
|
|
524
|
+
<p class="text-gray-300 italic mb-4">"AeCode redujo nuestros costos de tokens de IA en más de 80%. Esencial para cualquier agente."</p>
|
|
525
|
+
<span class="font-bold text-amber-400">— Sarah C., AI Lead</span>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
|
|
528
|
+
<p class="text-gray-300 italic mb-4">"Construir scrapers y dashboards en 3 líneas de código parece magia. 10/10."</p>
|
|
529
|
+
<span class="font-bold text-amber-400">— David R., Senior Engineer</span>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</section>''')
|
|
533
|
+
elif line.startswith('FORM'):
|
|
534
|
+
m = re.search(r'FORM\s+"([^"]+)"', line)
|
|
535
|
+
form_title = m.group(1) if m else "Suscríbete"
|
|
536
|
+
body_parts.append(f'''<section class="max-w-xl mx-auto px-6 py-10">
|
|
537
|
+
<div class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-xl text-center">
|
|
538
|
+
<h3 class="text-2xl font-bold text-white mb-4">{form_title}</h3>
|
|
539
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
540
|
+
<input type="email" placeholder="tu@email.com" class="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-amber-500">
|
|
541
|
+
<button class="px-6 py-3 bg-amber-500 hover:bg-amber-400 text-gray-950 font-bold rounded-xl transition">Enviar</button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</section>''')
|
|
545
|
+
elif line.startswith('CANVAS'):
|
|
546
|
+
has_canvas = True
|
|
547
|
+
m = re.search(r'CANVAS\s+(\d+)x(\d+)', line)
|
|
548
|
+
w = m.group(1) if m else "900"
|
|
549
|
+
h = m.group(2) if m else "550"
|
|
550
|
+
body_parts.append(f'''<div class="max-w-6xl mx-auto px-4 pt-6 pb-2 flex justify-center">
|
|
551
|
+
<canvas id="paintCanvas" width="{w}" height="{h}" class="bg-white rounded-2xl shadow-2xl border-4 border-gray-800 cursor-crosshair touch-none max-w-full block"></canvas>
|
|
552
|
+
</div>''')
|
|
553
|
+
elif line.startswith('PALETTE'):
|
|
554
|
+
has_canvas = True
|
|
555
|
+
content = extract_brackets(line)
|
|
556
|
+
colors = [c.strip() for c in split_items(content)]
|
|
557
|
+
swatches = []
|
|
558
|
+
for i, c in enumerate(colors):
|
|
559
|
+
active = "ring-4 ring-amber-400 scale-110" if i == 0 else ""
|
|
560
|
+
swatches.append(f'<button type="button" class="palette-swatch w-9 h-9 rounded-full border-2 border-gray-700 shadow-md transition-all duration-150 hover:scale-110 focus:outline-none {active}" data-color="{c}" style="background-color: {c};" title="{c}"></button>')
|
|
561
|
+
body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
562
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Color:</span>
|
|
563
|
+
<div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
564
|
+
{''.join(swatches)}
|
|
565
|
+
</div>
|
|
566
|
+
</div>''')
|
|
567
|
+
elif line.startswith('SIZES'):
|
|
568
|
+
has_canvas = True
|
|
569
|
+
content = extract_brackets(line)
|
|
570
|
+
sizes = [s.strip() for s in split_items(content)]
|
|
571
|
+
size_btns = []
|
|
572
|
+
for i, s in enumerate(sizes):
|
|
573
|
+
active = "bg-amber-500 text-gray-950 font-bold ring-2 ring-amber-400" if i == 1 or (len(sizes)==1 and i==0) else "bg-gray-800 text-gray-300 hover:bg-gray-700 font-semibold"
|
|
574
|
+
size_btns.append(f'<button type="button" class="size-btn px-4 py-2 rounded-xl text-sm transition-all duration-150 border border-gray-700 focus:outline-none {active}" data-size="{s}">{s}px</button>')
|
|
575
|
+
body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
576
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Grosor:</span>
|
|
577
|
+
<div class="flex flex-wrap items-center gap-2 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
578
|
+
{''.join(size_btns)}
|
|
579
|
+
</div>
|
|
580
|
+
</div>''')
|
|
581
|
+
elif line.startswith('TOOLS'):
|
|
582
|
+
has_canvas = True
|
|
583
|
+
content = extract_brackets(line)
|
|
584
|
+
tools = [t.strip().lower() for t in split_items(content)]
|
|
585
|
+
tool_icons = {
|
|
586
|
+
"brush": "✏️ Pincel",
|
|
587
|
+
"eraser": "🧹 Borrador",
|
|
588
|
+
"clear": "🗑️ Limpiar",
|
|
589
|
+
"download": "💾 Descargar"
|
|
590
|
+
}
|
|
591
|
+
tool_btns = []
|
|
592
|
+
for t in tools:
|
|
593
|
+
label = tool_icons.get(t, t.capitalize())
|
|
594
|
+
active = "bg-amber-500 text-gray-950 ring-2 ring-amber-400" if t == "brush" else "bg-gray-800 text-gray-200 hover:bg-gray-700"
|
|
595
|
+
tool_btns.append(f'<button type="button" class="tool-btn px-4 py-2 rounded-xl text-sm font-bold transition-all duration-150 border border-gray-700 focus:outline-none flex items-center gap-2 {active}" data-tool="{t}">{label}</button>')
|
|
596
|
+
body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
|
|
597
|
+
<span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Herramientas:</span>
|
|
598
|
+
<div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
|
|
599
|
+
{''.join(tool_btns)}
|
|
600
|
+
</div>
|
|
601
|
+
</div>''')
|
|
602
|
+
elif line.startswith('FOOTER'):
|
|
603
|
+
body_parts.append('''<footer class="bg-gray-900 text-white py-8 px-6 text-center border-t border-gray-800">
|
|
604
|
+
<p class="text-gray-400">Generado con AeCode</p>
|
|
605
|
+
</footer>''')
|
|
606
|
+
|
|
607
|
+
script_tag = _get_canvas_script() if has_canvas else ""
|
|
608
|
+
html = f"""<!DOCTYPE html>
|
|
609
|
+
<html lang="es">
|
|
610
|
+
<head>
|
|
611
|
+
<meta charset="UTF-8">
|
|
612
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
613
|
+
<title>{title}</title>
|
|
614
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
615
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
|
|
616
|
+
<style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body class="bg-gray-950 text-gray-100 min-h-screen pb-12">
|
|
619
|
+
{''.join(body_parts)}
|
|
620
|
+
{script_tag}
|
|
621
|
+
</body>
|
|
622
|
+
</html>"""
|
|
623
|
+
return html
|
|
624
|
+
|
|
625
|
+
def extract_brackets(text):
|
|
626
|
+
m = re.search(r'\[(.*?)\]', text)
|
|
627
|
+
return m.group(1) if m else ""
|
|
628
|
+
|
|
629
|
+
def extract_parens(text):
|
|
630
|
+
m = re.search(r'\((.*?)\)', text)
|
|
631
|
+
return m.group(1) if m else ""
|
|
632
|
+
|
|
633
|
+
def split_items(text):
|
|
634
|
+
items = []
|
|
635
|
+
current = []
|
|
636
|
+
depth = 0
|
|
637
|
+
for char in text:
|
|
638
|
+
if char == '(':
|
|
639
|
+
depth += 1
|
|
640
|
+
current.append(char)
|
|
641
|
+
elif char == ')':
|
|
642
|
+
depth -= 1
|
|
643
|
+
current.append(char)
|
|
644
|
+
elif char == ',' and depth == 0:
|
|
645
|
+
items.append(''.join(current).strip())
|
|
646
|
+
current = []
|
|
647
|
+
else:
|
|
648
|
+
current.append(char)
|
|
649
|
+
if current:
|
|
650
|
+
items.append(''.join(current).strip())
|
|
651
|
+
return [i for i in items if i]
|
|
652
|
+
|
|
653
|
+
def build_and_open(code, output="output.html"):
|
|
654
|
+
html = build_ui(code)
|
|
655
|
+
Path(output).write_text(html, encoding='utf-8')
|
|
656
|
+
print(f"Generado: {output}")
|
|
657
|
+
try:
|
|
658
|
+
webbrowser.open(Path(output).as_uri())
|
|
659
|
+
except:
|
|
660
|
+
pass
|
|
661
|
+
return html
|