Ricky01anjay commited on
Commit
ee43427
Β·
verified Β·
1 Parent(s): 5eb5439

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -129
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  """
3
  AI HTML Editor β€” Flask Server
4
  Semua pemrosesan (AI call, find/change, fuzzy match) dilakukan di sisi server.
@@ -6,7 +5,7 @@ Job disimpan di memori dengan TTL 1 hari.
6
  """
7
 
8
  from flask import Flask, request, jsonify, Response
9
- import uuid, time, re, threading, requests
10
 
11
  app = Flask(__name__)
12
 
@@ -49,24 +48,9 @@ def update_job(jid: str, **kwargs):
49
  JOBS[jid].update(kwargs)
50
 
51
  # ─────────────────────────────────────────────
52
- # ALGORITMA FIND & CHANGE
53
  # ─────────────────────────────────────────────
54
 
55
- def normalize_ai_text(text: str) -> str:
56
- """
57
- 1. Mengubah literal '\n', '\t', '\r' dari AI menjadi karakter whitespace asli.
58
- 2. Memperbaiki jika AI mengembalikan HTML yang ter-escape secara tidak sengaja.
59
- """
60
- # Ubah literal escape menjadi karakter sesungguhnya
61
- text = text.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
62
-
63
- # Decode HTML Entities
64
- return (text.replace('&', '&')
65
- .replace('&lt;', '<')
66
- .replace('&gt;', '>')
67
- .replace('&quot;', '"')
68
- .replace('&#39;', "'"))
69
-
70
  def levenshtein(a: str, b: str) -> int:
71
  la, lb = len(a), len(b)
72
  if la == 0: return lb
@@ -88,6 +72,7 @@ def calculate_similarity(s1: str, s2: str) -> float:
88
  return (len(longer) - levenshtein(longer, shorter)) / len(longer)
89
 
90
  def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
 
91
  search_trimmed = search.strip()
92
  if not search_trimmed:
93
  return {"success": False, "result": source}
@@ -104,7 +89,7 @@ def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
104
  except re.error:
105
  pass
106
 
107
- # Level 2 β€” sliding window + Levenshtein
108
  if len(search_trimmed) > 3000:
109
  return {"success": False, "result": source}
110
 
@@ -135,18 +120,24 @@ def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
135
 
136
  return {"success": False, "result": source}
137
 
 
138
  def apply_find_change(code_base: str, ai_response: str) -> dict:
 
 
 
 
139
  exec_match = re.search(r'<execution>([\s\S]*?)</execution>', ai_response, re.IGNORECASE)
140
  if not exec_match:
141
- raise ValueError("AI Gagal menggunakan format tag <execution>.")
142
 
143
  exec_content = exec_match.group(1)
 
144
  respond_match = re.search(r'<responding>([\s\S]*?)</responding>', exec_content, re.IGNORECASE)
145
  respond_text = respond_match.group(1).strip() if respond_match else "Kode berhasil diperbarui."
146
 
147
  find_blocks = re.findall(r'<find>([\s\S]*?)</find>', exec_content, re.IGNORECASE)
148
  if not find_blocks:
149
- raise ValueError("AI tidak memberikan tag <find> yang berisi <element> dan <change>.")
150
 
151
  temp_code = code_base
152
  success_count = 0
@@ -156,13 +147,16 @@ def apply_find_change(code_base: str, ai_response: str) -> dict:
156
  el_match = re.search(r'<element>([\s\S]*?)</element>', block, re.IGNORECASE)
157
  ch_match = re.search(r'<change>([\s\S]*?)</change>', block, re.IGNORECASE)
158
  if not el_match or not ch_match:
159
- raise ValueError(f"Blok <find> ke-{i+1} formatnya salah (kehilangan element/change).")
160
 
161
- # Normalisasi: Ubah literal \n jadi newline asli & decode entitas HTML
162
- old_code = normalize_ai_text(el_match.group(1))
163
- new_code = normalize_ai_text(ch_match.group(1))
164
-
165
  old_code_trimmed = old_code.strip()
 
 
 
 
 
 
166
 
167
  if old_code in temp_code:
168
  temp_code = temp_code.replace(old_code, new_code, 1)
@@ -178,43 +172,33 @@ def apply_find_change(code_base: str, ai_response: str) -> dict:
178
  is_fuzzy = True
179
  else:
180
  raise ValueError(
181
- f"Kode pada <element> ke-{i+1} TIDAK DITEMUKAN persis di Codebase.\n"
182
- f"Teks yang kamu kirim:\n{old_code[:100]}..."
 
183
  )
184
 
185
  action_label = f"Diperbarui ({success_count} bagian){' ✨ Auto-Fix' if is_fuzzy else ''}"
186
  return {"code": temp_code, "message": respond_text, "action_label": action_label}
187
 
 
188
  # ─────────────────────────────────────────────
189
  # AI CALL
190
  # ─────────────────────────────────────────────
191
- GEMINI_API = "https://api.siputzx.my.id/api/ai/gemini-lite"
192
- VALID_MODELS = {
193
- "gemini-3-flash-preview",
194
- "gemini-3.1-flash-lite-preview",
195
- "gemini-2.5-flash-lite",
196
- "gemini-2.5-flash",
197
- }
198
- DEFAULT_MODEL = "gemini-2.5-flash"
199
-
200
- def call_ai(prompt: str, model: str = DEFAULT_MODEL) -> str:
201
- if model not in VALID_MODELS:
202
- model = DEFAULT_MODEL
203
  resp = requests.post(
204
  GEMINI_API,
205
- json={"prompt": prompt, "model": model},
206
  timeout=60,
207
  headers={"Content-Type": "application/json"},
208
  )
209
  resp.raise_for_status()
210
  data = resp.json()
211
- if isinstance(data, dict):
212
- answer = data.get("data") or data.get("result") or data.get("answer") or data.get("response")
213
- if answer:
214
- if isinstance(answer, dict):
215
- answer = answer.get("answer") or answer.get("text") or str(answer)
216
- return str(answer)
217
- raise RuntimeError(f"Format respons API tidak dikenali: {str(data)[:200]}")
218
 
219
  def build_system_prompt(code_base: str) -> str:
220
  return f"""Kamu adalah AI HTML Editor. Tugasmu HANYA mengubah bagian tertentu dari kode (Partial Update).
@@ -223,13 +207,14 @@ Codebase saat ini:
223
  {code_base}
224
  ```
225
 
226
- ATURAN WAJIB PENGGUNAAN BLOK <find>:
227
- 1. <element> HARUS berisi teks/kode yang IDENTIK PERSIS dengan Codebase (termasuk spasi, tab, dan baris kosong). JANGAN perbaiki indentasi di dalam <element>!
228
- 2. JANGAN memasukkan potongan kode yang terlalu panjang. Ambil sependek mungkin (1-4 baris) asalkan UNIK.
229
- 3. <change> berisi kode baru yang akan menggantikan <element> tersebut.
230
- 4. Balas HANYA dengan format <execution> seperti contoh. DILARANG menulis ulang seluruh codebase di luar tag.
 
231
 
232
- CONTOH JAWABAN YANG BENAR:
233
  <execution>
234
  <responding>Saya telah mengganti teks H1 dan warna tombol.</responding>
235
  <find>
@@ -242,29 +227,25 @@ CONTOH JAWABAN YANG BENAR:
242
  </find>
243
  </execution>"""
244
 
 
245
  def run_ai_loop(jid: str, user_instruction: str, code_base: str,
246
- loop: int = 0, error_feedback: str = None, max_loops: int = 10,
247
- model: str = DEFAULT_MODEL):
248
- """Rekursif self-healing AI."""
249
  if loop >= max_loops:
250
  update_job(jid,
251
  status="error",
252
- error="Gagal merubah kode setelah 10x loop perbaikan. AI terus menerus gagal mencocokkan kode dengan Codebase. Mohon salin sebagian kecil kode secara manual ke dalam chat jika ingin diubah.",
253
  loop_count=loop,
254
  )
255
  return
256
 
257
  try:
258
  prompt = build_system_prompt(code_base) + f"\n\nInstruksi User: {user_instruction}"
259
-
260
  if error_feedback:
261
- prompt += (f"\n\n[PENTING! ERROR PADA JAWABANMU SEBELUMNYA]:\n{error_feedback}\n\n"
262
- "INSTRUKSI PERBAIKAN:\n"
263
- "1. Kesalahan ini terjadi karena teks di <element> beda spasi/karakter dengan codebase.\n"
264
- "2. SOLUSI: Ambil potongan kode yang LEBIH PENDEK (1-2 baris saja) yang unik.\n"
265
- "3. Jangan mengubah spasi dan indentasi sedikitpun saat menyalin ke <element>!")
266
 
267
- ai_text = call_ai(prompt, model=model)
268
  result = apply_find_change(code_base, ai_text)
269
 
270
  update_job(jid,
@@ -274,24 +255,27 @@ def run_ai_loop(jid: str, user_instruction: str, code_base: str,
274
  )
275
 
276
  except Exception as exc:
277
- # Self-healing: retry dengan mengirimkan error ke AI
278
  threading.Thread(
279
  target=run_ai_loop,
280
  args=(jid, user_instruction, code_base, loop + 1, str(exc), max_loops),
281
- kwargs={"model": model},
282
  daemon=True,
283
  ).start()
284
 
 
285
  # ─────────────────────────────────────────────
286
  # ROUTES
287
  # ─────────────────────────────────────────────
288
 
289
  @app.route("/api/chat", methods=["POST"])
290
  def api_chat():
 
 
 
 
291
  body = request.get_json(silent=True) or {}
292
  instruction = body.get("instruction", "").strip()
293
  code_base = body.get("code_base", "")
294
- model = body.get("model", DEFAULT_MODEL)
295
 
296
  if not instruction:
297
  return jsonify({"error": "instruction wajib diisi"}), 400
@@ -302,14 +286,15 @@ def api_chat():
302
  threading.Thread(
303
  target=run_ai_loop,
304
  args=(jid, instruction, code_base),
305
- kwargs={"model": model},
306
  daemon=True,
307
  ).start()
308
 
309
  return jsonify({"job_id": jid}), 202
310
 
 
311
  @app.route("/api/job/<jid>", methods=["GET"])
312
  def api_job_status(jid):
 
313
  job = get_job(jid)
314
  if not job:
315
  return jsonify({"error": "Job tidak ditemukan atau sudah expired (> 1 hari)"}), 404
@@ -326,14 +311,17 @@ def api_job_status(jid):
326
 
327
  return jsonify(resp)
328
 
 
329
  @app.route("/api/job/<jid>", methods=["DELETE"])
330
  def api_delete_job(jid):
 
331
  with _lock:
332
  if jid in JOBS:
333
  del JOBS[jid]
334
  return jsonify({"deleted": True})
335
  return jsonify({"error": "Job tidak ditemukan"}), 404
336
 
 
337
  # ─────────────────────────────────────────────
338
  # UI (embedded HTML)
339
  # ─────────────────────────────────────────────
@@ -358,34 +346,18 @@ HTML = r"""<!DOCTYPE html>
358
  <div x-data="aiEditor()" class="w-full max-w-md h-full bg-white shadow-2xl relative flex flex-col">
359
 
360
  <!-- Header -->
361
- <header class="flex-shrink-0 bg-indigo-600 text-white shadow-md z-10">
362
- <div class="flex justify-between items-center px-4 pt-3 pb-2">
363
- <h1 class="text-lg font-bold flex items-center gap-2">
364
- <i class="fa-solid fa-wand-magic-sparkles"></i> AI Editor
365
- </h1>
366
- <div class="flex gap-2">
367
- <button @click="clearChat" title="Hapus riwayat chat" class="bg-indigo-500 hover:bg-red-500 p-2 rounded text-sm transition font-medium shadow">
368
- <i class="fa-solid fa-trash-can"></i>
369
- </button>
370
- <button @click="downloadCode" class="bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
371
- <i class="fa-solid fa-file-download"></i> Simpan
372
- </button>
373
- <label class="cursor-pointer bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
374
- <i class="fa-solid fa-file-upload"></i> Upload
375
- <input type="file" accept=".html" class="hidden" @change="uploadFile">
376
- </label>
377
- </div>
378
- </div>
379
- <div class="px-4 pb-2 flex items-center gap-2">
380
- <i class="fa-solid fa-robot text-indigo-300 text-xs"></i>
381
- <span class="text-indigo-200 text-xs">Model:</span>
382
- <select x-model="selectedModel" @change="saveModel"
383
- class="flex-1 bg-indigo-700 text-white text-xs rounded px-2 py-1 outline-none border border-indigo-400 cursor-pointer">
384
- <option value="gemini-3-flash-preview">Gemini 3 Flash Preview</option>
385
- <option value="gemini-3.1-flash-lite-preview">Gemini 3.1 Flash Lite Preview</option>
386
- <option value="gemini-2.5-flash-lite">Gemini 2.5 Flash Lite</option>
387
- <option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
388
- </select>
389
  </div>
390
  </header>
391
 
@@ -526,17 +498,10 @@ HTML = r"""<!DOCTYPE html>
526
 
527
  <script>
528
  document.addEventListener('alpine:init', () => {
529
- const LS_CODE = 'aiEditor_codeBase';
530
- const LS_CHAT = 'aiEditor_chatHistory';
531
- const LS_MODEL = 'aiEditor_model';
532
- const DEFAULT_CODE = `<!DOCTYPE html>\n<html>\n<head>\n <title>Halo Dunia</title>\n</head>\n<body>\n <h1>Halo Dunia!</h1>\n <button onclick="showAlert()">Klik Saya</button>\n <script>\n function showAlert() {\n alert('Halo!');\n }\n <\/script>\n</body>\n</html>`;
533
- const DEFAULT_CHAT = [{role:'ai', content:'Halo! Saya AI Editor (Server-Side). Minta saya mengubah bagian mana saja di halaman ini!'}];
534
-
535
  Alpine.data('aiEditor', () => ({
536
  tab: 'chat',
537
- codeBase: localStorage.getItem(LS_CODE) || DEFAULT_CODE,
538
- chatHistory: (() => { try { return JSON.parse(localStorage.getItem(LS_CHAT)) || DEFAULT_CHAT; } catch { return DEFAULT_CHAT; } })(),
539
- selectedModel: localStorage.getItem(LS_MODEL) || 'gemini-2.5-flash',
540
  userInput: '',
541
  isProcessing: false,
542
  loopCount: 0,
@@ -546,13 +511,7 @@ HTML = r"""<!DOCTYPE html>
546
 
547
  init() {
548
  this.$watch('tab', val => { if (val === 'preview') this.updatePreview(); });
549
- this.$watch('codeBase', val => {
550
- localStorage.setItem(LS_CODE, val);
551
- if (this.tab === 'preview') this.updatePreview();
552
- });
553
- this.$watch('chatHistory', val => {
554
- try { localStorage.setItem(LS_CHAT, JSON.stringify(val)); } catch {}
555
- });
556
  window.addEventListener('message', event => {
557
  if (event.data && event.data.type === 'PREVIEW_ERROR') {
558
  this.previewError = this.previewError
@@ -562,18 +521,6 @@ HTML = r"""<!DOCTYPE html>
562
  });
563
  },
564
 
565
- saveModel() {
566
- localStorage.setItem(LS_MODEL, this.selectedModel);
567
- this.showToast('Model: ' + this.selectedModel);
568
- },
569
-
570
- clearChat() {
571
- if (!confirm('Hapus semua riwayat chat?')) return;
572
- this.chatHistory = [{role:'ai', content:'Halo! Saya AI Editor (Server-Side). Minta saya mengubah bagian mana saja di halaman ini!'}];
573
- localStorage.removeItem(LS_CHAT);
574
- this.showToast('Riwayat chat dihapus!');
575
- },
576
-
577
  showToast(msg) {
578
  this.toastMsg = msg;
579
  setTimeout(() => this.toastMsg = '', 3000);
@@ -623,14 +570,13 @@ HTML = r"""<!DOCTYPE html>
623
  const errorCatcherScript = `<script>
624
  (function(){
625
  var _all=[],_t=null;
626
- function _isTailwind(m){return/tailwind|cdn\\.tailwindcss/i.test(m);}
627
  function _report(d){_all.push(d);clearTimeout(_t);_t=setTimeout(function(){if(_all.length)window.parent.postMessage({type:'PREVIEW_ERROR',error:_all.join('\\n\\n---\\n\\n')},'*');},200);}
628
  function _fmt(e){if(!e)return'';var o=e.name?e.name+': '+e.message:String(e);if(e.stack){o+='\\n Stack:\\n '+e.stack.split('\\n').slice(0,5).join('\\n ');}return o;}
629
- window.onerror=function(m,s,l,c,e){if(m==='Script error.'||m==='Script error')return true;var d;if(e){d='[Runtime Error]\\n'+_fmt(e);if(s)d+='\\n File: '+s;if(l)d+='\\n Line: '+l+(c?', Col: '+c:'');}else{d='[Runtime Error] '+m;if(s)d+='\\n File: '+s;if(l)d+='\\n Line: '+l+(c?', Col: '+c:'');}_report(d);return true;};
630
  window.addEventListener('unhandledrejection',function(e){var r=e.reason,d='[Unhandled Promise Rejection]\\n';if(r instanceof Error){d+=_fmt(r);}else{d+=String(r);}_report(d);});
631
  var _oe=console.error.bind(console),_ow=console.warn.bind(console);
632
  console.error=function(){var m=Array.from(arguments).map(function(a){return a instanceof Error?_fmt(a):(typeof a==='object'?JSON.stringify(a,null,2):String(a));}).join(' ');_report('[console.error] '+m);_oe.apply(console,arguments);};
633
- console.warn=function(){var m=Array.from(arguments).map(function(a){return typeof a==='object'?JSON.stringify(a):String(a);}).join(' ');if(_isTailwind(m))return;_report('[console.warn] '+m);_ow.apply(console,arguments);};
634
  var _of=window.fetch;window.fetch=function(url,opts){return _of.apply(this,arguments).then(function(r){if(!r.ok)_report('[Fetch Error] '+(opts&&opts.method||'GET')+' '+url+'\\n Status: '+r.status+' '+r.statusText);return r;}).catch(function(err){_report('[Fetch Failed] '+url+'\\n '+_fmt(err));throw err;});};
635
  var _oc=document.createElement.bind(document);document.createElement=function(tag){var el=_oc(tag);if(tag.toLowerCase()==='script')el.addEventListener('error',function(){_report('[Script Load Error] Gagal memuat: '+(el.src||'unknown'));});return el;};
636
  })();
@@ -669,7 +615,7 @@ HTML = r"""<!DOCTYPE html>
669
  const res = await fetch('/api/chat', {
670
  method: 'POST',
671
  headers: {'Content-Type': 'application/json'},
672
- body: JSON.stringify({instruction, code_base: this.codeBase, model: this.selectedModel}),
673
  });
674
  const data = await res.json();
675
  if (!res.ok) throw new Error(data.error || 'Server error');
@@ -728,5 +674,6 @@ HTML = r"""<!DOCTYPE html>
728
  def index():
729
  return Response(HTML, mimetype="text/html")
730
 
 
731
  if __name__ == "__main__":
732
  app.run(host="0.0.0.0", port=7860)
 
 
1
  """
2
  AI HTML Editor β€” Flask Server
3
  Semua pemrosesan (AI call, find/change, fuzzy match) dilakukan di sisi server.
 
5
  """
6
 
7
  from flask import Flask, request, jsonify, Response
8
+ import uuid, time, re, json, threading, requests
9
 
10
  app = Flask(__name__)
11
 
 
48
  JOBS[jid].update(kwargs)
49
 
50
  # ─────────────────────────────────────────────
51
+ # ALGORITMA FIND & CHANGE (ported dari JS)
52
  # ─────────────────────────────────────────────
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def levenshtein(a: str, b: str) -> int:
55
  la, lb = len(a), len(b)
56
  if la == 0: return lb
 
72
  return (len(longer) - levenshtein(longer, shorter)) / len(longer)
73
 
74
  def fuzzy_replace(source: str, search: str, replacement: str) -> dict:
75
+ """Sama persis dengan fuzzyReplace() di JS."""
76
  search_trimmed = search.strip()
77
  if not search_trimmed:
78
  return {"success": False, "result": source}
 
89
  except re.error:
90
  pass
91
 
92
+ # Level 2 β€” sliding window + Levenshtein (hanya jika < 3000 chars)
93
  if len(search_trimmed) > 3000:
94
  return {"success": False, "result": source}
95
 
 
120
 
121
  return {"success": False, "result": source}
122
 
123
+
124
  def apply_find_change(code_base: str, ai_response: str) -> dict:
125
+ """
126
+ Parse respons AI dan terapkan semua blok <find>/<element>/<change>.
127
+ Return dict: {success, code, message, action_label}
128
+ """
129
  exec_match = re.search(r'<execution>([\s\S]*?)</execution>', ai_response, re.IGNORECASE)
130
  if not exec_match:
131
+ raise ValueError("Gunakan format tag <execution>.")
132
 
133
  exec_content = exec_match.group(1)
134
+
135
  respond_match = re.search(r'<responding>([\s\S]*?)</responding>', exec_content, re.IGNORECASE)
136
  respond_text = respond_match.group(1).strip() if respond_match else "Kode berhasil diperbarui."
137
 
138
  find_blocks = re.findall(r'<find>([\s\S]*?)</find>', exec_content, re.IGNORECASE)
139
  if not find_blocks:
140
+ raise ValueError("Kamu wajib menggunakan tag <find> berisi <element> dan <change>.")
141
 
142
  temp_code = code_base
143
  success_count = 0
 
147
  el_match = re.search(r'<element>([\s\S]*?)</element>', block, re.IGNORECASE)
148
  ch_match = re.search(r'<change>([\s\S]*?)</change>', block, re.IGNORECASE)
149
  if not el_match or not ch_match:
150
+ raise ValueError(f"Blok <find> ke-{i+1} tidak memiliki <element> atau <change>.")
151
 
152
+ old_code = el_match.group(1)
 
 
 
153
  old_code_trimmed = old_code.strip()
154
+ new_code = (ch_match.group(1)
155
+ .replace('&amp;', '&')
156
+ .replace('&lt;', '<')
157
+ .replace('&gt;', '>')
158
+ .replace('&quot;', '"')
159
+ .replace('&#39;', "'"))
160
 
161
  if old_code in temp_code:
162
  temp_code = temp_code.replace(old_code, new_code, 1)
 
172
  is_fuzzy = True
173
  else:
174
  raise ValueError(
175
+ f"Isi dari <element> pada <find> ke-{i+1}:\n{old_code}\n\n"
176
+ "TIDAK DITEMUKAN di codebase meskipun fitur Similarity Fallback sudah aktif. "
177
+ "Salin teks dengan lebih teliti!"
178
  )
179
 
180
  action_label = f"Diperbarui ({success_count} bagian){' ✨ Auto-Fix' if is_fuzzy else ''}"
181
  return {"code": temp_code, "message": respond_text, "action_label": action_label}
182
 
183
+
184
  # ─────────────────────────────────────────────
185
  # AI CALL
186
  # ─────────────────────────────────────────────
187
+ GEMINI_API = "https://puruboy-api.vercel.app/api/ai/gemini-v2"
188
+
189
+ def call_ai(prompt: str) -> str:
 
 
 
 
 
 
 
 
 
190
  resp = requests.post(
191
  GEMINI_API,
192
+ json={"prompt": prompt},
193
  timeout=60,
194
  headers={"Content-Type": "application/json"},
195
  )
196
  resp.raise_for_status()
197
  data = resp.json()
198
+ if not data.get("success"):
199
+ raise RuntimeError("Gagal terhubung ke API / Server Error.")
200
+ return data["result"]["answer"]
201
+
 
 
 
202
 
203
  def build_system_prompt(code_base: str) -> str:
204
  return f"""Kamu adalah AI HTML Editor. Tugasmu HANYA mengubah bagian tertentu dari kode (Partial Update).
 
207
  {code_base}
208
  ```
209
 
210
+ ATURAN WAJIB:
211
+ 1. Gunakan blok <find> untuk setiap perubahan.
212
+ 2. <element> HARUS berisi teks/kode lama yang IDENTIK PERSIS dengan Codebase.
213
+ 3. <change> berisi kode baru penggantinya.
214
+ 4. Boleh memakai banyak blok <find> sekaligus.
215
+ 5. Balas HANYA dengan format <execution> seperti contoh di bawah. DILARANG KERAS menulis ulang seluruh kode!
216
 
217
+ CONTOH JAWABAN:
218
  <execution>
219
  <responding>Saya telah mengganti teks H1 dan warna tombol.</responding>
220
  <find>
 
227
  </find>
228
  </execution>"""
229
 
230
+
231
  def run_ai_loop(jid: str, user_instruction: str, code_base: str,
232
+ loop: int = 0, error_feedback: str = None, max_loops: int = 10):
233
+ """Rekursif di thread terpisah β€” sama dengan runAILoop() JS."""
 
234
  if loop >= max_loops:
235
  update_job(jid,
236
  status="error",
237
+ error="Gagal merubah kode setelah 10x loop perbaikan. AI terus menerus gagal mencocokkan kode dengan Codebase. Coba beri instruksi yang lebih jelas.",
238
  loop_count=loop,
239
  )
240
  return
241
 
242
  try:
243
  prompt = build_system_prompt(code_base) + f"\n\nInstruksi User: {user_instruction}"
 
244
  if error_feedback:
245
+ prompt += (f"\n\n[ERROR PADA JAWABANMU SEBELUMNYA]:\n{error_feedback}\n"
246
+ "Perbaiki pencarian teksmu. Jika kau kesulitan, setidaknya salin elemen persis seperti aslinya.")
 
 
 
247
 
248
+ ai_text = call_ai(prompt)
249
  result = apply_find_change(code_base, ai_text)
250
 
251
  update_job(jid,
 
255
  )
256
 
257
  except Exception as exc:
258
+ # Self-healing: retry
259
  threading.Thread(
260
  target=run_ai_loop,
261
  args=(jid, user_instruction, code_base, loop + 1, str(exc), max_loops),
 
262
  daemon=True,
263
  ).start()
264
 
265
+
266
  # ─────────────────────────────────────────────
267
  # ROUTES
268
  # ─────────────────────────────────────────────
269
 
270
  @app.route("/api/chat", methods=["POST"])
271
  def api_chat():
272
+ """
273
+ Body: { instruction: str, code_base: str }
274
+ Return: { job_id: str }
275
+ """
276
  body = request.get_json(silent=True) or {}
277
  instruction = body.get("instruction", "").strip()
278
  code_base = body.get("code_base", "")
 
279
 
280
  if not instruction:
281
  return jsonify({"error": "instruction wajib diisi"}), 400
 
286
  threading.Thread(
287
  target=run_ai_loop,
288
  args=(jid, instruction, code_base),
 
289
  daemon=True,
290
  ).start()
291
 
292
  return jsonify({"job_id": jid}), 202
293
 
294
+
295
  @app.route("/api/job/<jid>", methods=["GET"])
296
  def api_job_status(jid):
297
+ """Poll status job."""
298
  job = get_job(jid)
299
  if not job:
300
  return jsonify({"error": "Job tidak ditemukan atau sudah expired (> 1 hari)"}), 404
 
311
 
312
  return jsonify(resp)
313
 
314
+
315
  @app.route("/api/job/<jid>", methods=["DELETE"])
316
  def api_delete_job(jid):
317
+ """Hapus job secara manual."""
318
  with _lock:
319
  if jid in JOBS:
320
  del JOBS[jid]
321
  return jsonify({"deleted": True})
322
  return jsonify({"error": "Job tidak ditemukan"}), 404
323
 
324
+
325
  # ─────────────────────────────────────────────
326
  # UI (embedded HTML)
327
  # ─────────────────────────────────────────────
 
346
  <div x-data="aiEditor()" class="w-full max-w-md h-full bg-white shadow-2xl relative flex flex-col">
347
 
348
  <!-- Header -->
349
+ <header class="flex-shrink-0 bg-indigo-600 text-white p-4 flex justify-between items-center shadow-md z-10">
350
+ <h1 class="text-lg font-bold flex items-center gap-2">
351
+ <i class="fa-solid fa-wand-magic-sparkles"></i> AI Editor
352
+ </h1>
353
+ <div class="flex gap-2">
354
+ <button @click="downloadCode" class="bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
355
+ <i class="fa-solid fa-file-download"></i> Simpan
356
+ </button>
357
+ <label class="cursor-pointer bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
358
+ <i class="fa-solid fa-file-upload"></i> Upload
359
+ <input type="file" accept=".html" class="hidden" @change="uploadFile">
360
+ </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  </div>
362
  </header>
363
 
 
498
 
499
  <script>
500
  document.addEventListener('alpine:init', () => {
 
 
 
 
 
 
501
  Alpine.data('aiEditor', () => ({
502
  tab: 'chat',
503
+ codeBase: `<!DOCTYPE html>\n<html>\n<head>\n <title>Halo Dunia</title>\n</head>\n<body>\n <h1>Halo Dunia!</h1>\n <button onclick="showAlert()">Klik Saya</button>\n <script>\n function showAlert() {\n alert('Halo!');\n }\n <\/script>\n</body>\n</html>`,
504
+ chatHistory: [{role:'ai', content:'Halo! Saya AI Editor (Server-Side). Minta saya mengubah bagian mana saja di halaman ini!'}],
 
505
  userInput: '',
506
  isProcessing: false,
507
  loopCount: 0,
 
511
 
512
  init() {
513
  this.$watch('tab', val => { if (val === 'preview') this.updatePreview(); });
514
+ this.$watch('codeBase', () => { if (this.tab === 'preview') this.updatePreview(); });
 
 
 
 
 
 
515
  window.addEventListener('message', event => {
516
  if (event.data && event.data.type === 'PREVIEW_ERROR') {
517
  this.previewError = this.previewError
 
521
  });
522
  },
523
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  showToast(msg) {
525
  this.toastMsg = msg;
526
  setTimeout(() => this.toastMsg = '', 3000);
 
570
  const errorCatcherScript = `<script>
571
  (function(){
572
  var _all=[],_t=null;
 
573
  function _report(d){_all.push(d);clearTimeout(_t);_t=setTimeout(function(){if(_all.length)window.parent.postMessage({type:'PREVIEW_ERROR',error:_all.join('\\n\\n---\\n\\n')},'*');},200);}
574
  function _fmt(e){if(!e)return'';var o=e.name?e.name+': '+e.message:String(e);if(e.stack){o+='\\n Stack:\\n '+e.stack.split('\\n').slice(0,5).join('\\n ');}return o;}
575
+ window.onerror=function(m,s,l,c,e){var d;if(e){d='[Runtime Error]\\n'+_fmt(e);if(s)d+='\\n File: '+s;if(l)d+='\\n Line: '+l+(c?', Col: '+c:'');}else if(m==='Script error.'||m==='Script error'){d='[Cross-Origin Script Error]\\n Browser menyembunyikan detail.\\n File: '+(s||'unknown');}else{d='[Runtime Error] '+m;if(s)d+='\\n File: '+s;if(l)d+='\\n Line: '+l+(c?', Col: '+c:'');}_report(d);return true;};
576
  window.addEventListener('unhandledrejection',function(e){var r=e.reason,d='[Unhandled Promise Rejection]\\n';if(r instanceof Error){d+=_fmt(r);}else{d+=String(r);}_report(d);});
577
  var _oe=console.error.bind(console),_ow=console.warn.bind(console);
578
  console.error=function(){var m=Array.from(arguments).map(function(a){return a instanceof Error?_fmt(a):(typeof a==='object'?JSON.stringify(a,null,2):String(a));}).join(' ');_report('[console.error] '+m);_oe.apply(console,arguments);};
579
+ console.warn=function(){var m=Array.from(arguments).map(function(a){return typeof a==='object'?JSON.stringify(a):String(a);}).join(' ');_report('[console.warn] '+m);_ow.apply(console,arguments);};
580
  var _of=window.fetch;window.fetch=function(url,opts){return _of.apply(this,arguments).then(function(r){if(!r.ok)_report('[Fetch Error] '+(opts&&opts.method||'GET')+' '+url+'\\n Status: '+r.status+' '+r.statusText);return r;}).catch(function(err){_report('[Fetch Failed] '+url+'\\n '+_fmt(err));throw err;});};
581
  var _oc=document.createElement.bind(document);document.createElement=function(tag){var el=_oc(tag);if(tag.toLowerCase()==='script')el.addEventListener('error',function(){_report('[Script Load Error] Gagal memuat: '+(el.src||'unknown'));});return el;};
582
  })();
 
615
  const res = await fetch('/api/chat', {
616
  method: 'POST',
617
  headers: {'Content-Type': 'application/json'},
618
+ body: JSON.stringify({instruction, code_base: this.codeBase}),
619
  });
620
  const data = await res.json();
621
  if (!res.ok) throw new Error(data.error || 'Server error');
 
674
  def index():
675
  return Response(HTML, mimetype="text/html")
676
 
677
+
678
  if __name__ == "__main__":
679
  app.run(host="0.0.0.0", port=7860)