TheCoderScientist commited on
Commit
7e4b910
·
verified ·
1 Parent(s): 101f6a3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +653 -0
app.py ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # 🦅 GarudaCoder-27B — Indonesian Coding Intelligence
3
+ # Precision Engineered Interface (Hugging Face ZeroGPU Edition)
4
+ # ==============================================================================
5
+
6
+ import os
7
+ os.environ["HF_HUB_DISABLE_XET"] = "1"
8
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
9
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
10
+ os.environ["FORCE_QWENVL_VIDEO_READER"] = "torchvision"
11
+
12
+ import gc
13
+ import re
14
+ import io
15
+ import time
16
+ import spaces
17
+ import threading
18
+ import traceback
19
+ from pathlib import Path
20
+
21
+ import torch
22
+ import gradio as gr
23
+ from transformers import (
24
+ AutoConfig,
25
+ AutoProcessor,
26
+ BitsAndBytesConfig,
27
+ TextIteratorStreamer,
28
+ )
29
+ from peft import PeftModel
30
+
31
+ try:
32
+ from transformers import AutoModelForMultimodalLM
33
+ except ImportError:
34
+ try:
35
+ from transformers import AutoModelForImageTextToText as AutoModelForMultimodalLM
36
+ except ImportError:
37
+ from transformers import AutoModelForVision2Seq as AutoModelForMultimodalLM
38
+
39
+ # ==============================================================================
40
+ # 1. KONFIGURASI MODEL & PROMPT
41
+ # ==============================================================================
42
+ BASE_ID = "unsloth/Qwen3.8-27B-unsloth-bnb-4bit"
43
+ ADAPTER_ID = "TheCoderScientist/GarudaCoder-27B-ID-lora"
44
+ EOS_IDS = [248046, 248044]
45
+
46
+ SYSTEM_PROMPT = """Kamu adalah GarudaCoder, asisten coding berbahasa Indonesia yang teliti.
47
+
48
+ Prioritas:
49
+ - Utamakan kebenaran teknis dan kode yang dapat diuji.
50
+ - Berikan ringkasan alasan yang dapat diaudit sebelum solusi.
51
+ - Nyatakan asumsi dan edge case yang relevan.
52
+ - Jangan mengarang fakta, API, versi, angka, dokumentasi, atau hasil eksekusi.
53
+ - Bila informasi tidak cukup, katakan apa yang kurang.
54
+ - Gunakan konteks gambar, video, file, atau hasil web search yang diberikan.
55
+ - Bedakan fakta dari inferensi.
56
+ - Untuk informasi yang berubah atau bersifat lokal, gunakan sumber yang dapat diverifikasi.
57
+ - Jangan menampilkan chain-of-thought privat.
58
+ - Jangan mengklaim telah menjalankan kode jika memang belum dijalankan.
59
+ - Identitas kamu adalah GarudaCoder.
60
+ - Jika ditanya basis model, jelaskan secara ringkas bahwa GarudaCoder menggunakan Qwen3.8-27B sebagai base model dengan adapter GarudaCoder.
61
+ """
62
+
63
+ print("[Init] Menyiapkan Processor & Tokenizer...")
64
+ processor = AutoProcessor.from_pretrained(ADAPTER_ID, trust_remote_code=True)
65
+ tokenizer = getattr(processor, "tokenizer", processor)
66
+
67
+ if getattr(tokenizer, "pad_token", None) is None:
68
+ tokenizer.pad_token = tokenizer.eos_token
69
+
70
+ _model = None
71
+
72
+ def get_model():
73
+ """ZeroGPU Lazy Loader: Muat model hanya saat inferensi diminta."""
74
+ global _model
75
+ if _model is None:
76
+ print("[ZeroGPU] Memuat bobot 4-bit ke NVIDIA A100...")
77
+ bnb_config = BitsAndBytesConfig(
78
+ load_in_4bit=True,
79
+ bnb_4bit_quant_type="nf4",
80
+ bnb_4bit_compute_dtype=torch.float16,
81
+ )
82
+ base_model = AutoModelForMultimodalLM.from_pretrained(
83
+ BASE_ID,
84
+ quantization_config=bnb_config,
85
+ device_map="auto",
86
+ attn_implementation="sdpa",
87
+ trust_remote_code=True,
88
+ )
89
+ print("[ZeroGPU] Mengaktifkan adapter LoRA GarudaCoder...")
90
+ _model = PeftModel.from_pretrained(
91
+ base_model,
92
+ ADAPTER_ID,
93
+ is_trainable=False,
94
+ )
95
+ _model.eval()
96
+ print("[ZeroGPU] Alokasi VRAM selesai.")
97
+ return _model
98
+
99
+ # ==============================================================================
100
+ # 2. FILE & MULTIMODAL PARSERS
101
+ # ==============================================================================
102
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
103
+ VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".webm"}
104
+ TEXT_EXTENSIONS = {
105
+ ".txt", ".md", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".c", ".cpp",
106
+ ".h", ".hpp", ".cs", ".go", ".rs", ".php", ".rb", ".swift", ".kt", ".sh",
107
+ ".sql", ".json", ".yaml", ".yml", ".toml", ".csv", ".log", ".html", ".css"
108
+ }
109
+
110
+ def normalize_path(file_obj):
111
+ if file_obj is None:
112
+ return None
113
+ if isinstance(file_obj, str):
114
+ return file_obj
115
+ if isinstance(file_obj, dict):
116
+ return file_obj.get("path") or file_obj.get("name")
117
+ return getattr(file_obj, "path", None) or getattr(file_obj, "name", None)
118
+
119
+ def file_uri(path):
120
+ return Path(os.path.abspath(path)).as_uri()
121
+
122
+ def extract_text_file(path, max_chars=40000):
123
+ try:
124
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
125
+ return f.read(max_chars)
126
+ except Exception as e:
127
+ return f"[Gagal membaca file teks: {e}]"
128
+
129
+ def extract_pdf(path, max_chars=40000):
130
+ try:
131
+ from pypdf import PdfReader
132
+ reader = PdfReader(path)
133
+ chunks = []
134
+ for page in reader.pages:
135
+ txt = page.extract_text() or ""
136
+ chunks.append(txt)
137
+ if sum(len(x) for x in chunks) >= max_chars:
138
+ break
139
+ return "\n\n".join(chunks)[:max_chars]
140
+ except Exception as e:
141
+ return f"[Gagal membaca PDF: {e}]"
142
+
143
+ def extract_docx(path, max_chars=40000):
144
+ try:
145
+ from docx import Document
146
+ doc = Document(path)
147
+ parts = [p.text for p in doc.paragraphs if p.text.strip()]
148
+ return "\n".join(parts)[:max_chars]
149
+ except Exception as e:
150
+ return f"[Gagal membaca DOCX: {e}]"
151
+
152
+ def extract_xlsx(path, max_chars=40000):
153
+ try:
154
+ from openpyxl import load_workbook
155
+ wb = load_workbook(path, read_only=True, data_only=True)
156
+ parts = []
157
+ for ws in wb.worksheets:
158
+ parts.append(f"## Sheet: {ws.title}")
159
+ for row in ws.iter_rows(values_only=True):
160
+ vals = ["" if v is None else str(v) for v in row]
161
+ parts.append(" | ".join(vals))
162
+ if sum(len(x) for x in parts) >= max_chars:
163
+ break
164
+ return "\n".join(parts)[:max_chars]
165
+ except Exception as e:
166
+ return f"[Gagal membaca XLSX: {e}]"
167
+
168
+ def inspect_uploaded_files(files):
169
+ vision_blocks = []
170
+ text_parts = []
171
+ summary = []
172
+
173
+ for raw in files or []:
174
+ path = normalize_path(raw)
175
+ if not path or not os.path.exists(path):
176
+ continue
177
+
178
+ name = os.path.basename(path)
179
+ ext = Path(path).suffix.lower()
180
+
181
+ if ext in IMAGE_EXTENSIONS:
182
+ vision_blocks.append({"type": "image", "image": file_uri(path)})
183
+ summary.append(name)
184
+ elif ext in VIDEO_EXTENSIONS:
185
+ vision_blocks.append({"type": "video", "video": file_uri(path), "fps": 1.0})
186
+ summary.append(name)
187
+ elif ext in TEXT_EXTENSIONS:
188
+ text_parts.append(f"### [FILE: {name}]\n```text\n{extract_text_file(path)}\n```")
189
+ summary.append(name)
190
+ elif ext == ".pdf":
191
+ text_parts.append(f"### [PDF: {name}]\n{extract_pdf(path)}")
192
+ summary.append(name)
193
+ elif ext == ".docx":
194
+ text_parts.append(f"### [DOCX: {name}]\n{extract_docx(path)}")
195
+ summary.append(name)
196
+ elif ext in {".xlsx", ".xlsm"}:
197
+ text_parts.append(f"### [SPREADSHEET: {name}]\n{extract_xlsx(path)}")
198
+ summary.append(name)
199
+
200
+ return vision_blocks, "\n\n".join(text_parts), summary
201
+
202
+ # ==============================================================================
203
+ # 3. FAST WEB SEARCH (DUCKDUCKGO)
204
+ # ==============================================================================
205
+ def web_search(query, max_results=4):
206
+ query = (query or "").strip()
207
+ if not query:
208
+ return []
209
+ try:
210
+ from duckduckgo_search import DDGS
211
+ results = []
212
+ with DDGS() as ddgs:
213
+ for r in ddgs.text(query, max_results=max_results, region="wt-wt", safesearch="moderate"):
214
+ results.append({
215
+ "title": r.get("title") or "Dokumentasi",
216
+ "url": r.get("href") or "",
217
+ "snippet": r.get("body") or "",
218
+ })
219
+ return results
220
+ except Exception as e:
221
+ print(f"[Web Search Warn] {e}")
222
+ return []
223
+
224
+ def format_search_context(results):
225
+ if not results:
226
+ return ""
227
+ chunks = ["## HASIL DOKUMENTASI/WEB TERBARU:"]
228
+ for i, r in enumerate(results, 1):
229
+ chunks.append(f"[{i}] {r['title']} ({r['url']})\nSnippet: {r['snippet']}")
230
+ return "\n\n".join(chunks)
231
+
232
+ def sources_html(results):
233
+ if not results:
234
+ return '<div class="sources-empty">Pencarian web nonaktif atau belum ada kueri.</div>'
235
+ chips = []
236
+ for i, r in enumerate(results, 1):
237
+ title = gr.utils.sanitize_html(r["title"][:75])
238
+ url = r["url"]
239
+ domain = re.sub(r"^https?://(www\.)?", "", url).split("/")[0]
240
+ chips.append(
241
+ f'<a class="source-chip" href="{url}" target="_blank" title="{title}">'
242
+ f'<span class="source-domain">{domain}</span>'
243
+ f'<span class="source-title">{title}</span>'
244
+ f'</a>'
245
+ )
246
+ return f'<div class="sources-grid">{"".join(chips)}</div>'
247
+
248
+ # ==============================================================================
249
+ # 4. CHAT GENERATION ENGINE
250
+ # ==============================================================================
251
+ @spaces.GPU(duration=120)
252
+ def generate_chat(
253
+ message,
254
+ history,
255
+ system_prompt,
256
+ temperature,
257
+ top_p,
258
+ max_new_tokens,
259
+ enable_thinking,
260
+ enable_web_search,
261
+ ):
262
+ active_model = get_model()
263
+
264
+ user_text = (message or {}).get("text", "").strip()
265
+ files = (message or {}).get("files", [])
266
+
267
+ vision_blocks, file_context, file_names = inspect_uploaded_files(files)
268
+
269
+ search_results = []
270
+ if enable_web_search and user_text:
271
+ search_results = web_search(user_text, max_results=4)
272
+ search_context = format_search_context(search_results)
273
+
274
+ sys_content = system_prompt.strip() if system_prompt else SYSTEM_PROMPT
275
+ messages = [{"role": "system", "content": sys_content}]
276
+
277
+ for turn in history or []:
278
+ role = turn.get("role")
279
+ content = turn.get("content")
280
+ if role in {"user", "assistant"} and isinstance(content, str):
281
+ messages.append({"role": role, "content": content})
282
+
283
+ current_turn_content = []
284
+ current_turn_content.extend(vision_blocks)
285
+
286
+ context_sections = []
287
+ if file_context:
288
+ context_sections.append(file_context)
289
+ if search_context:
290
+ context_sections.append(search_context)
291
+
292
+ if context_sections:
293
+ full_text = "\n\n".join(context_sections) + (f"\n\n## INSTRUKSI USER:\n{user_text}" if user_text else "")
294
+ current_turn_content.append({"type": "text", "text": full_text})
295
+ elif user_text:
296
+ current_turn_content.append({"type": "text", "text": user_text})
297
+ elif not vision_blocks:
298
+ current_turn_content.append({"type": "text", "text": "Halo!"})
299
+
300
+ messages.append({"role": "user", "content": current_turn_content})
301
+
302
+ try:
303
+ model_inputs = processor.apply_chat_template(
304
+ messages,
305
+ add_generation_prompt=True,
306
+ tokenize=True,
307
+ return_dict=True,
308
+ return_tensors="pt",
309
+ enable_thinking=bool(enable_thinking),
310
+ )
311
+ except TypeError:
312
+ model_inputs = processor.apply_chat_template(
313
+ messages,
314
+ add_generation_prompt=True,
315
+ tokenize=True,
316
+ return_dict=True,
317
+ return_tensors="pt",
318
+ )
319
+
320
+ model_inputs = {k: v.to("cuda") for k, v in model_inputs.items() if torch.is_tensor(v)}
321
+
322
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
323
+ gen_kwargs = dict(
324
+ **model_inputs,
325
+ streamer=streamer,
326
+ max_new_tokens=int(max_new_tokens),
327
+ eos_token_id=EOS_IDS,
328
+ pad_token_id=tokenizer.pad_token_id,
329
+ repetition_penalty=1.05,
330
+ )
331
+
332
+ if float(temperature) > 0.05:
333
+ gen_kwargs.update(do_sample=True, temperature=float(temperature), top_p=float(top_p), top_k=20)
334
+ else:
335
+ gen_kwargs.update(do_sample=False)
336
+
337
+ errors = []
338
+ def _generate():
339
+ try:
340
+ with torch.inference_mode():
341
+ active_model.generate(**gen_kwargs)
342
+ except Exception as e:
343
+ errors.append(e)
344
+ traceback.print_exc()
345
+ finally:
346
+ streamer.end()
347
+
348
+ thread = threading.Thread(target=_generate, daemon=True)
349
+ thread.start()
350
+
351
+ badges = []
352
+ if file_names:
353
+ badges.append(f"Attachment: {', '.join(file_names[:2])}{'...' if len(file_names) > 2 else ''}")
354
+ if search_results:
355
+ badges.append(f"Web Search: {len(search_results)} rujukan")
356
+
357
+ meta_tag = f'<div class="context-tag">{" · ".join(badges)}</div>\n\n' if badges else ""
358
+ output = ""
359
+
360
+ for chunk in streamer:
361
+ output += chunk
362
+ yield meta_tag + output, search_results
363
+
364
+ thread.join()
365
+
366
+ if errors:
367
+ yield output + f"\n\n> ⚠️ **Runtime Issue:** `{errors[0]}`", search_results
368
+
369
+ # ==============================================================================
370
+ # 5. MODERN UI SYSTEM (LINEAR / CURSOR AESTHETICS)
371
+ # ==============================================================================
372
+ WELCOME_MD = """### Siap membantu audit kode dan arsitektur sistem.
373
+
374
+ Pilih topik di bawah atau lampirkan file dokumen/gambar langsung:
375
+ """
376
+
377
+ CSS = r"""
378
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
379
+
380
+ :root {
381
+ --bg-base: #0a0c10;
382
+ --bg-surface: #11141b;
383
+ --bg-subtle: #181d26;
384
+ --border-subtle: rgba(255, 255, 255, 0.07);
385
+ --border-active: rgba(255, 255, 255, 0.15);
386
+ --text-primary: #f0f3f6;
387
+ --text-secondary: #8c96a5;
388
+ --text-muted: #566171;
389
+ --accent: #10b981;
390
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
391
+ --font-mono: 'JetBrains Mono', monospace;
392
+ }
393
+
394
+ body, .gradio-container {
395
+ background-color: var(--bg-base) !important;
396
+ color: var(--text-primary) !important;
397
+ font-family: var(--font-sans) !important;
398
+ max-width: 1380px !important;
399
+ margin: 0 auto !important;
400
+ }
401
+
402
+ /* Header Navbar */
403
+ .app-nav {
404
+ display: flex;
405
+ justify-content: space-between;
406
+ align-items: center;
407
+ padding: 18px 0;
408
+ border-bottom: 1px solid var(--border-subtle);
409
+ margin-bottom: 16px;
410
+ }
411
+ .brand-group {
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 12px;
415
+ }
416
+ .brand-badge {
417
+ background: var(--bg-subtle);
418
+ border: 1px solid var(--border-subtle);
419
+ border-radius: 8px;
420
+ padding: 6px 9px;
421
+ font-size: 14px;
422
+ font-weight: 700;
423
+ }
424
+ .brand-name {
425
+ font-size: 16px;
426
+ font-weight: 600;
427
+ letter-spacing: -0.02em;
428
+ color: var(--text-primary);
429
+ }
430
+ .brand-desc {
431
+ font-size: 12px;
432
+ color: var(--text-secondary);
433
+ }
434
+ .status-pill {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ gap: 6px;
438
+ font-size: 11px;
439
+ font-family: var(--font-mono);
440
+ color: var(--text-secondary);
441
+ background: var(--bg-surface);
442
+ border: 1px solid var(--border-subtle);
443
+ padding: 4px 10px;
444
+ border-radius: 9999px;
445
+ }
446
+ .status-dot {
447
+ width: 6px;
448
+ height: 6px;
449
+ border-radius: 50%;
450
+ background-color: var(--accent);
451
+ }
452
+
453
+ /* Chatbox Area */
454
+ .chat-viewport {
455
+ background: var(--bg-surface) !important;
456
+ border: 1px solid var(--border-subtle) !important;
457
+ border-radius: 12px !important;
458
+ }
459
+
460
+ /* Source Reference Chips (Perplexity Style) */
461
+ .sources-grid {
462
+ display: flex;
463
+ flex-wrap: wrap;
464
+ gap: 8px;
465
+ padding: 4px 0;
466
+ }
467
+ .source-chip {
468
+ display: inline-flex;
469
+ flex-direction: column;
470
+ background: var(--bg-subtle);
471
+ border: 1px solid var(--border-subtle);
472
+ border-radius: 8px;
473
+ padding: 6px 10px;
474
+ text-decoration: none !important;
475
+ transition: border-color 0.15s ease, background 0.15s ease;
476
+ max-width: 100%;
477
+ }
478
+ .source-chip:hover {
479
+ border-color: var(--border-active);
480
+ background: #1e2430;
481
+ }
482
+ .source-domain {
483
+ font-size: 10px;
484
+ font-family: var(--font-mono);
485
+ color: var(--accent);
486
+ text-transform: uppercase;
487
+ letter-spacing: 0.04em;
488
+ }
489
+ .source-title {
490
+ font-size: 11px;
491
+ font-weight: 500;
492
+ color: var(--text-primary) !important;
493
+ white-space: nowrap;
494
+ overflow: hidden;
495
+ text-overflow: ellipsis;
496
+ max-width: 240px;
497
+ }
498
+ .sources-empty {
499
+ font-size: 12px;
500
+ color: var(--text-muted);
501
+ font-style: italic;
502
+ }
503
+
504
+ /* Context Badge in Response */
505
+ .context-tag {
506
+ display: inline-block;
507
+ font-family: var(--font-mono);
508
+ font-size: 10px;
509
+ color: var(--text-secondary);
510
+ background: var(--bg-subtle);
511
+ border: 1px solid var(--border-subtle);
512
+ border-radius: 4px;
513
+ padding: 3px 6px;
514
+ margin-bottom: 6px;
515
+ }
516
+
517
+ /* Prompt Suggestion Chips */
518
+ .quick-prompt-row {
519
+ display: flex;
520
+ gap: 8px;
521
+ margin-bottom: 12px;
522
+ flex-wrap: wrap;
523
+ }
524
+ .quick-prompt-btn {
525
+ background: var(--bg-surface) !important;
526
+ border: 1px solid var(--border-subtle) !important;
527
+ color: var(--text-secondary) !important;
528
+ font-size: 11px !important;
529
+ border-radius: 20px !important;
530
+ padding: 4px 12px !important;
531
+ cursor: pointer !important;
532
+ font-weight: 400 !important;
533
+ transition: all 0.15s ease !important;
534
+ }
535
+ .quick-prompt-btn:hover {
536
+ color: var(--text-primary) !important;
537
+ border-color: var(--border-active) !important;
538
+ background: var(--bg-subtle) !important;
539
+ }
540
+
541
+ /* Sidebar Box Minimalist */
542
+ .sidebar-panel {
543
+ background: var(--bg-surface) !important;
544
+ border: 1px solid var(--border-subtle) !important;
545
+ border-radius: 12px !important;
546
+ padding: 16px !important;
547
+ }
548
+ """
549
+
550
+ with gr.Blocks(title="GarudaCoder-27B", css=CSS, theme=gr.themes.Base()) as demo:
551
+ # ------------------ TOP NAV BAR ------------------
552
+ gr.HTML("""
553
+ <div class="app-nav">
554
+ <div class="brand-group">
555
+ <div class="brand-badge">🦅 GC-27B</div>
556
+ <div>
557
+ <div class="brand-name">GarudaCoder Workspace</div>
558
+ <div class="brand-desc">Indonesian Specialized Coding Intelligence · Qwen3.8 Architecture</div>
559
+ </div>
560
+ </div>
561
+ <div class="status-pill">
562
+ <span class="status-dot"></span>
563
+ <span>ZeroGPU A100 · 4-Bit Native</span>
564
+ </div>
565
+ </div>
566
+ """)
567
+
568
+ with gr.Row():
569
+ # ------------------ SETTINGS / SIDEBAR ------------------
570
+ with gr.Column(scale=1, min_width=270):
571
+ with gr.Group(elem_classes="sidebar-panel"):
572
+ gr.Markdown("#### Runtime Controls")
573
+ enable_thinking = gr.Checkbox(value=False, label="Deep Reasoning (CoT)")
574
+ enable_web_search = gr.Checkbox(value=False, label="Live Web Context (DDG)")
575
+
576
+ with gr.Accordion("Sampling & Temperature", open=False):
577
+ temperature = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="Temperature")
578
+ top_p = gr.Slider(0.1, 1.0, value=0.85, step=0.05, label="Top-P")
579
+ max_new_tokens = gr.Slider(128, 4096, value=1024, step=64, label="Max Tokens")
580
+
581
+ with gr.Accordion("System Instructions", open=False):
582
+ system_prompt = gr.Textbox(value=SYSTEM_PROMPT, label="Prompt", lines=6, show_label=False)
583
+
584
+ with gr.Group(elem_classes="sidebar-panel", visible=True):
585
+ gr.Markdown("#### Web References")
586
+ sources_box = gr.HTML('<div class="sources-empty">Pencarian web nonaktif.</div>')
587
+
588
+ # ------------------ CHAT WORKSPACE ------------------
589
+ with gr.Column(scale=3):
590
+ chatbot = gr.Chatbot(
591
+ type="messages",
592
+ value=[{"role": "assistant", "content": WELCOME_MD}],
593
+ height=620,
594
+ show_label=False,
595
+ elem_classes="chat-viewport"
596
+ )
597
+
598
+ # Quick Prompt Suggestions (Interactive Pills)
599
+ with gr.Row(elem_classes="quick-prompt-row"):
600
+ p1 = gr.Button("Memory leak pada Python generator?", elem_classes="quick-prompt-btn")
601
+ p2 = gr.Button("Arsitektur Microservices vs Modular Monolith", elem_classes="quick-prompt-btn")
602
+ p3 = gr.Button("Optimasi SQL Query (N+1 Problem)", elem_classes="quick-prompt-btn")
603
+
604
+ message = gr.MultimodalTextbox(
605
+ placeholder="Tanyakan analisis kode, upload dokumen teknis (PDF/Docx/Sheet), atau screenshot error...",
606
+ file_types=["image", "video", "file"],
607
+ file_count="multiple",
608
+ submit_btn="Send",
609
+ stop_btn="Stop",
610
+ )
611
+
612
+ # ------------------ INTERAKSI & EVENTS ------------------
613
+ def set_prompt_p1():
614
+ return {"text": "Bagaimana cara mendeteksi dan mencegah memory leak pada penggunaan generator & closure di Python?", "files": []}
615
+ def set_prompt_p2():
616
+ return {"text": "Kapan sebaiknya beralih dari Modular Monolith ke Microservices? Berikan trade-off engineeringnya.", "files": []}
617
+ def set_prompt_p3():
618
+ return {"text": "Jelaskan apa itu masalah N+1 Query pada ORM dan 3 strategi konkret untuk mengatasinya.", "files": []}
619
+
620
+ p1.click(set_prompt_p1, outputs=[message])
621
+ p2.click(set_prompt_p2, outputs=[message])
622
+ p3.click(set_prompt_p3, outputs=[message])
623
+
624
+ def stream_handler(msg, hist, sp, temp, tp, mnt, eth, ews):
625
+ user_text = (msg or {}).get("text", "")
626
+ files = (msg or {}).get("files", [])
627
+
628
+ user_content_display = []
629
+ for f in files:
630
+ path = normalize_path(f)
631
+ if path:
632
+ user_content_display.append({"path": path})
633
+ if user_text:
634
+ user_content_display.append(user_text)
635
+
636
+ new_history = list(hist or [])
637
+ new_history.append({"role": "user", "content": user_content_display if user_content_display else user_text})
638
+ new_history.append({"role": "assistant", "content": ""})
639
+
640
+ last_search = []
641
+ for partial_text, search_res in generate_chat(msg, hist, sp, temp, tp, mnt, eth, ews):
642
+ new_history[-1]["content"] = partial_text
643
+ last_search = search_res
644
+ yield new_history, sources_html(last_search)
645
+
646
+ message.submit(
647
+ stream_handler,
648
+ inputs=[message, chatbot, system_prompt, temperature, top_p, max_new_tokens, enable_thinking, enable_web_search],
649
+ outputs=[chatbot, sources_box]
650
+ )
651
+
652
+ if __name__ == "__main__":
653
+ demo.queue(max_size=16, default_concurrency_limit=1).launch()