Ricky01anjay commited on
Commit
a153b34
Β·
verified Β·
1 Parent(s): ca58e57

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -27
app.py CHANGED
@@ -184,20 +184,33 @@ def apply_find_change(code_base: str, ai_response: str) -> dict:
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:
@@ -229,7 +242,8 @@ CONTOH JAWABAN:
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,
@@ -245,7 +259,7 @@ def run_ai_loop(jid: str, user_instruction: str, code_base: str,
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,
@@ -259,6 +273,7 @@ def run_ai_loop(jid: str, user_instruction: str, code_base: str,
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
 
@@ -276,6 +291,7 @@ def api_chat():
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,6 +302,7 @@ def api_chat():
286
  threading.Thread(
287
  target=run_ai_loop,
288
  args=(jid, instruction, code_base),
 
289
  daemon=True,
290
  ).start()
291
 
@@ -346,18 +363,34 @@ HTML = r"""<!DOCTYPE html>
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,10 +531,17 @@ HTML = r"""<!DOCTYPE html>
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,7 +551,13 @@ HTML = r"""<!DOCTYPE html>
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,6 +567,18 @@ HTML = r"""<!DOCTYPE html>
521
  });
522
  },
523
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  showToast(msg) {
525
  this.toastMsg = msg;
526
  setTimeout(() => this.toastMsg = '', 3000);
@@ -570,13 +628,14 @@ HTML = r"""<!DOCTYPE html>
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,7 +674,7 @@ HTML = r"""<!DOCTYPE html>
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');
 
184
  # ─────────────────────────────────────────────
185
  # AI CALL
186
  # ─────────────────────────────────────────────
187
+ GEMINI_API = "https://api.siputzx.my.id/api/ai/gemini-lite"
188
+ VALID_MODELS = {
189
+ "gemini-3-flash-preview",
190
+ "gemini-3.1-flash-lite-preview",
191
+ "gemini-2.5-flash-lite",
192
+ "gemini-2.5-flash",
193
+ }
194
+ DEFAULT_MODEL = "gemini-2.5-flash"
195
+
196
+ def call_ai(prompt: str, model: str = DEFAULT_MODEL) -> str:
197
+ if model not in VALID_MODELS:
198
+ model = DEFAULT_MODEL
199
  resp = requests.post(
200
  GEMINI_API,
201
+ json={"prompt": prompt, "model": model},
202
  timeout=60,
203
  headers={"Content-Type": "application/json"},
204
  )
205
  resp.raise_for_status()
206
  data = resp.json()
207
+ if isinstance(data, dict):
208
+ answer = data.get("data") or data.get("result") or data.get("answer") or data.get("response")
209
+ if answer:
210
+ if isinstance(answer, dict):
211
+ answer = answer.get("answer") or answer.get("text") or str(answer)
212
+ return str(answer)
213
+ raise RuntimeError(f"Format respons API tidak dikenali: {str(data)[:200]}")
214
 
215
 
216
  def build_system_prompt(code_base: str) -> str:
 
242
 
243
 
244
  def run_ai_loop(jid: str, user_instruction: str, code_base: str,
245
+ loop: int = 0, error_feedback: str = None, max_loops: int = 10,
246
+ model: str = DEFAULT_MODEL):
247
  """Rekursif di thread terpisah β€” sama dengan runAILoop() JS."""
248
  if loop >= max_loops:
249
  update_job(jid,
 
259
  prompt += (f"\n\n[ERROR PADA JAWABANMU SEBELUMNYA]:\n{error_feedback}\n"
260
  "Perbaiki pencarian teksmu. Jika kau kesulitan, setidaknya salin elemen persis seperti aslinya.")
261
 
262
+ ai_text = call_ai(prompt, model=model)
263
  result = apply_find_change(code_base, ai_text)
264
 
265
  update_job(jid,
 
273
  threading.Thread(
274
  target=run_ai_loop,
275
  args=(jid, user_instruction, code_base, loop + 1, str(exc), max_loops),
276
+ kwargs={"model": model},
277
  daemon=True,
278
  ).start()
279
 
 
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
  threading.Thread(
303
  target=run_ai_loop,
304
  args=(jid, instruction, code_base),
305
+ kwargs={"model": model},
306
  daemon=True,
307
  ).start()
308
 
 
363
  <div x-data="aiEditor()" class="w-full max-w-md h-full bg-white shadow-2xl relative flex flex-col">
364
 
365
  <!-- Header -->
366
+ <header class="flex-shrink-0 bg-indigo-600 text-white shadow-md z-10">
367
+ <div class="flex justify-between items-center px-4 pt-3 pb-2">
368
+ <h1 class="text-lg font-bold flex items-center gap-2">
369
+ <i class="fa-solid fa-wand-magic-sparkles"></i> AI Editor
370
+ </h1>
371
+ <div class="flex gap-2">
372
+ <button @click="clearChat" title="Hapus riwayat chat" class="bg-indigo-500 hover:bg-red-500 p-2 rounded text-sm transition font-medium shadow">
373
+ <i class="fa-solid fa-trash-can"></i>
374
+ </button>
375
+ <button @click="downloadCode" class="bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
376
+ <i class="fa-solid fa-file-download"></i> Simpan
377
+ </button>
378
+ <label class="cursor-pointer bg-indigo-500 hover:bg-indigo-400 p-2 rounded text-sm transition font-medium shadow">
379
+ <i class="fa-solid fa-file-upload"></i> Upload
380
+ <input type="file" accept=".html" class="hidden" @change="uploadFile">
381
+ </label>
382
+ </div>
383
+ </div>
384
+ <div class="px-4 pb-2 flex items-center gap-2">
385
+ <i class="fa-solid fa-robot text-indigo-300 text-xs"></i>
386
+ <span class="text-indigo-200 text-xs">Model:</span>
387
+ <select x-model="selectedModel" @change="saveModel"
388
+ class="flex-1 bg-indigo-700 text-white text-xs rounded px-2 py-1 outline-none border border-indigo-400 cursor-pointer">
389
+ <option value="gemini-3-flash-preview">Gemini 3 Flash Preview</option>
390
+ <option value="gemini-3.1-flash-lite-preview">Gemini 3.1 Flash Lite Preview</option>
391
+ <option value="gemini-2.5-flash-lite">Gemini 2.5 Flash Lite</option>
392
+ <option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
393
+ </select>
394
  </div>
395
  </header>
396
 
 
531
 
532
  <script>
533
  document.addEventListener('alpine:init', () => {
534
+ const LS_CODE = 'aiEditor_codeBase';
535
+ const LS_CHAT = 'aiEditor_chatHistory';
536
+ const LS_MODEL = 'aiEditor_model';
537
+ 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>`;
538
+ const DEFAULT_CHAT = [{role:'ai', content:'Halo! Saya AI Editor (Server-Side). Minta saya mengubah bagian mana saja di halaman ini!'}];
539
+
540
  Alpine.data('aiEditor', () => ({
541
  tab: 'chat',
542
+ codeBase: localStorage.getItem(LS_CODE) || DEFAULT_CODE,
543
+ chatHistory: (() => { try { return JSON.parse(localStorage.getItem(LS_CHAT)) || DEFAULT_CHAT; } catch { return DEFAULT_CHAT; } })(),
544
+ selectedModel: localStorage.getItem(LS_MODEL) || 'gemini-2.5-flash',
545
  userInput: '',
546
  isProcessing: false,
547
  loopCount: 0,
 
551
 
552
  init() {
553
  this.$watch('tab', val => { if (val === 'preview') this.updatePreview(); });
554
+ this.$watch('codeBase', val => {
555
+ localStorage.setItem(LS_CODE, val);
556
+ if (this.tab === 'preview') this.updatePreview();
557
+ });
558
+ this.$watch('chatHistory', val => {
559
+ try { localStorage.setItem(LS_CHAT, JSON.stringify(val)); } catch {}
560
+ });
561
  window.addEventListener('message', event => {
562
  if (event.data && event.data.type === 'PREVIEW_ERROR') {
563
  this.previewError = this.previewError
 
567
  });
568
  },
569
 
570
+ saveModel() {
571
+ localStorage.setItem(LS_MODEL, this.selectedModel);
572
+ this.showToast('Model: ' + this.selectedModel);
573
+ },
574
+
575
+ clearChat() {
576
+ if (!confirm('Hapus semua riwayat chat?')) return;
577
+ this.chatHistory = [{role:'ai', content:'Halo! Saya AI Editor (Server-Side). Minta saya mengubah bagian mana saja di halaman ini!'}];
578
+ localStorage.removeItem(LS_CHAT);
579
+ this.showToast('Riwayat chat dihapus!');
580
+ },
581
+
582
  showToast(msg) {
583
  this.toastMsg = msg;
584
  setTimeout(() => this.toastMsg = '', 3000);
 
628
  const errorCatcherScript = `<script>
629
  (function(){
630
  var _all=[],_t=null;
631
+ function _isTailwind(m){return/tailwind|cdn\\.tailwindcss/i.test(m);}
632
  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);}
633
  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;}
634
+ 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;};
635
  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);});
636
  var _oe=console.error.bind(console),_ow=console.warn.bind(console);
637
  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);};
638
+ 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);};
639
  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;});};
640
  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;};
641
  })();
 
674
  const res = await fetch('/api/chat', {
675
  method: 'POST',
676
  headers: {'Content-Type': 'application/json'},
677
+ body: JSON.stringify({instruction, code_base: this.codeBase, model: this.selectedModel}),
678
  });
679
  const data = await res.json();
680
  if (!res.ok) throw new Error(data.error || 'Server error');