Spaces:
Paused
Paused
Update app.py
Browse files
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://
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 350 |
-
<
|
| 351 |
-
<
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
<
|
| 355 |
-
<
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
<
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 504 |
-
chatHistory:
|
|
|
|
| 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',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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');
|