ErNewdev0 commited on
Commit
2ecdd91
·
verified ·
1 Parent(s): 870d523

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +741 -303
app.py CHANGED
@@ -16,15 +16,17 @@ import dotenv
16
  # Load environment variables
17
  dotenv.load_dotenv()
18
 
19
- # Metadata
20
- CURRENT_TIME = "2025-05-23 12:57:22"
21
- CURRENT_USER = "ErRickow"
22
 
23
  # Default API Keys (fallback if user doesn't provide their own)
24
  DEFAULT_XAI_KEY = os.getenv(
25
- "XAI_API_KEY"
 
26
  )
27
  DEFAULT_GEMINI_KEY = os.getenv("GEMINI_API_KEY")
 
28
 
29
  # API settings
30
  OLLAMA_API = os.environ.get("OLLAMA_API", "http://localhost:11434")
@@ -113,6 +115,40 @@ Note:
113
  """
114
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  class AIProvider:
117
  OLLAMA = "ollama"
118
  GEMINI = "gemini"
@@ -125,21 +161,20 @@ class RepoAnalyzer:
125
  self.repo_content = {}
126
  self.chat_history = []
127
 
128
- async def stream_xai_response(
129
- self, prompt: str, api_key: str = None, model: str = "grok-2-latest"
130
  ) -> AsyncGenerator[str, None]:
131
- """Stream response dari X.AI (Grok) API"""
132
  try:
133
- # Use default key if none provided
134
- actual_key = api_key if api_key else DEFAULT_XAI_KEY
135
-
136
  if not actual_key:
137
- yield "⚠️ API Key X.AI diperlukan. Gunakan key Anda sendiri atau tunggu reset limit default key."
138
  return
139
 
140
- client = AsyncOpenAI(api_key=actual_key, base_url=XAI_BASE_URL)
 
141
 
142
- # Prepare messages with repository context if available
143
  messages = [
144
  {
145
  "role": "system",
@@ -151,71 +186,84 @@ class RepoAnalyzer:
151
  context = f"Repository: {self.current_repo}\n\n"
152
  repo_files = "\n".join(list(self.repo_content.keys()))
153
  context += f"Files in repository:\n{repo_files}\n\n"
154
- messages.append({"role": "system", "content": context})
155
 
156
  messages.append({"role": "user", "content": prompt})
157
 
158
- stream = await client.chat.completions.create(
159
- model=model, messages=messages, stream=True
160
- )
 
 
 
 
 
 
161
 
162
- full_response = ""
163
- async for chunk in stream:
164
- if chunk.choices[0].delta.content:
165
- content = chunk.choices[0].delta.content
166
- full_response += content
167
- yield content
168
 
169
- self.chat_history.append({"role": "user", "content": prompt})
170
- self.chat_history.append({"role": "assistant", "content": full_response})
 
 
 
 
 
 
171
 
172
  except Exception as e:
173
- error_msg = f"⚠️ Error dalam X.AI API: {str(e)}"
174
  print(error_msg)
175
  yield error_msg
176
-
177
- async def stream_gemini_response(
178
- self, prompt: str, api_key: str
179
  ) -> AsyncGenerator[str, None]:
180
- """Stream response dari Gemini API"""
181
  try:
182
- if not api_key:
183
- yield "⚠️ API Key Gemini diperlukan."
 
184
  return
185
-
186
- genai.configure(api_key=api_key)
187
- model = genai.GenerativeModel("gemini-1.5-flash")
188
-
189
- # Tambahkan konteks repository jika ada
190
- if self.current_repo:
191
- context = f"Repository: {self.current_repo}\n\n"
192
- repo_files = "\n".join(list(self.repo_content.keys()))
193
- context += f"Files in repository:\n{repo_files}\n\n"
194
- prompt = context + prompt
195
-
196
- response = model.generate_content(
197
- prompt,
198
- generation_config={"temperature": 0.7, "top_p": 0.8, "top_k": 40},
199
- stream=True,
200
- )
201
-
202
- full_response = ""
203
- async for chunk in response:
204
- if chunk.text:
205
- full_response += chunk.text
206
- yield chunk.text
207
-
208
- self.chat_history.append({"role": "user", "content": prompt})
209
- self.chat_history.append({"role": "assistant", "content": full_response})
210
-
 
 
 
 
 
 
 
211
  except Exception as e:
212
- error_msg = f"⚠️ Error dalam Gemini API: {str(e)}\n\nPastikan API Key valid dan memiliki kuota yang cukup."
213
- print(error_msg)
214
- yield error_msg
215
 
216
- def clone_repository(
217
- self, repo_url: str, github_token: str, branch: str = None
218
- ) -> tuple[bool, str]:
219
  """Clone repository GitHub dengan autentikasi"""
220
  if not repo_url:
221
  return False, "⚠️ URL repository diperlukan"
@@ -230,9 +278,7 @@ class RepoAnalyzer:
230
 
231
  # Cek apakah repository private
232
  headers = {"Authorization": f"token {github_token}"} if github_token else {}
233
- repo_check = requests.get(
234
- f"https://api.github.com/repos/{owner_repo}", headers=headers
235
- )
236
 
237
  if repo_check.status_code == 404:
238
  return False, "⚠️ Repository tidak ditemukan. Periksa URL repository."
@@ -241,9 +287,7 @@ class RepoAnalyzer:
241
  False,
242
  "⚠️ Token GitHub tidak valid. Klik icon bantuan (?) untuk panduan mendapatkan token.",
243
  )
244
- elif repo_check.status_code == 403 and repo_check.json().get(
245
- "private", False
246
- ):
247
  return (
248
  False,
249
  "⚠️ Ini adalah repository private. Token GitHub dengan akses 'repo' diperlukan.",
@@ -300,8 +344,10 @@ class RepoAnalyzer:
300
  continue
301
  return False, "Tidak dapat membaca file dengan encoding yang didukung"
302
 
 
303
  analyzer = RepoAnalyzer()
304
 
 
305
  async def handle_chat(
306
  message,
307
  history,
@@ -313,173 +359,420 @@ async def handle_chat(
313
  analyzer=analyzer,
314
  ):
315
  """Menangani interaksi chat dengan model AI"""
316
- if not analyzer.current_repo:
317
- new_message = {
318
- "role": "assistant",
319
- "content": "⚠️ Mohon clone repository terlebih dahulu sebelum mengajukan pertanyaan.",
320
- }
321
- history = history or []
322
- history.append({"role": "user", "content": message})
323
- history.append(new_message)
324
- yield history
325
- return
 
326
 
327
- history = history or []
328
- history.append({"role": "user", "content": message})
329
- history.append({"role": "assistant", "content": ""})
 
330
 
331
- try:
332
- # Add context about selected files to the prompt
333
  file_context = ""
334
  if selected_files:
335
  file_context = "\n\nFile yang dipilih:\n"
336
  for file in selected_files:
337
  content = analyzer.repo_content.get(file, "")
338
- if content: # Only include files that exist
339
  file_context += f"\n{file}:\n```\n{content}\n```\n"
340
 
341
  enhanced_message = f"{message}\n{file_context}"
342
-
343
  full_response = ""
344
- if provider_choice == AIProvider.XAI:
345
- async for chunk in analyzer.stream_xai_response(
346
- enhanced_message, xai_key, model_name
347
- ):
348
- full_response += chunk
349
- # Add delay between chunks for readability
350
- await asyncio.sleep(1)
351
- history[-1]["content"] = full_response
352
- yield history
353
-
354
- elif provider_choice == AIProvider.GEMINI:
355
- async for chunk in analyzer.stream_gemini_response(
356
- enhanced_message, gemini_key or DEFAULT_GEMINI_KEY
357
- ):
358
- full_response += chunk
359
- # Add delay between chunks for readability
360
- await asyncio.sleep(0.5)
361
- history[-1]["content"] = full_response
362
- yield history
363
 
364
- else: # OLLAMA
365
- response = analyze_with_ollama(model_name, enhanced_message)
366
- # Simulate streaming for OLLAMA with delay
367
- words = response.split()
368
- for i in range(len(words)):
369
- full_response = " ".join(words[: i + 1])
370
- await asyncio.sleep(0.5)
371
- history[-1]["content"] = full_response
372
- yield history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  except Exception as e:
375
- history[-1]["content"] = f"⚠️ Error: {str(e)}"
 
376
  yield history
377
 
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  def create_ui():
380
- # Gunakan analyzer global
381
  global analyzer
382
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
383
 
384
  with gr.Blocks(title="Open Repo AI", theme=gr.themes.Soft()) as app:
385
  # CSS Styling
386
  gr.Markdown("""
387
  <style>
 
388
  .container { max-width: 100% !important; padding: 1rem; }
389
  .mobile-full { width: 100% !important; }
390
- .file-list { margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
391
- .file-item { display: flex; justify-content: space-between; padding: 5px 0; }
392
- .file-remove { color: red; cursor: pointer; }
393
- .example-list {
394
- padding: 20px;
 
395
  background: #f8f9fa;
396
- border-radius: 8px;
397
- border: 1px solid #e9ecef;
398
  margin: 10px 0;
 
399
  }
400
- .example-list h3 {
401
- color: #2c3e50;
402
- margin-top: 20px;
403
- margin-bottom: 10px;
404
- font-size: 1.2em;
 
 
 
 
 
 
 
 
405
  }
406
- .example-list h4 {
407
- color: #34495e;
408
- margin-top: 15px;
409
- margin-bottom: 5px;
410
- font-size: 1.1em;
411
  }
412
- .example-list ul {
 
 
413
  margin: 10px 0;
414
- padding-left: 20px;
 
 
415
  }
416
- .example-list li {
417
- margin: 8px 0;
418
- line-height: 1.5;
419
- list-style-type: disc;
 
 
 
 
420
  }
421
- .example-list code {
422
  background: #e9ecef;
423
- padding: 2px 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  border-radius: 4px;
425
- font-family: monospace;
426
- font-size: 0.9em;
427
  }
428
- .example-list strong {
429
- color: #2c3e50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  font-weight: bold;
 
431
  }
432
- .example-list p {
433
- margin: 10px 0;
434
- line-height: 1.5;
 
 
 
 
 
 
 
 
 
 
435
  }
 
 
 
 
 
 
 
 
 
436
  @media (max-width: 768px) {
437
  .gr-form { flex-direction: column !important; }
438
  .gr-group { margin: 0.5rem 0 !important; }
439
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  """)
442
 
443
  # Header
444
  with gr.Row(elem_classes="container"):
445
  gr.Markdown(f"""
446
- # AI Github Repository Chat
447
 
448
- - Current Date and Time (UTC): {current_time}
 
 
449
  """)
450
 
451
  # Main Tabs Container
452
  with gr.Tabs() as tabs:
453
  # Configuration Tab
454
- with gr.Tab("Konfigurasi"):
455
  provider = gr.Radio(
456
  choices=[AIProvider.XAI, AIProvider.GEMINI, AIProvider.OLLAMA],
457
- label="AI Providers",
458
  value=AIProvider.XAI,
459
  )
460
 
461
  with gr.Group() as api_settings:
 
462
  with gr.Row():
463
- xai_key = gr.Textbox(
464
- label="X.AI (Grok) API Key (opsional)",
465
- type="password",
466
- placeholder="Memakai Apikey Kamu Sendiri",
467
- show_label=True,
468
- scale=3,
469
- )
470
  with gr.Column(scale=1):
471
- gr.Markdown(XAI_API_HELP)
472
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  with gr.Row():
474
- gemini_key = gr.Textbox(
475
- label="Gemini API Key",
476
- type="password",
477
- placeholder="Opsional - Kosongkan untuk gunakan key default",
478
- show_label=True,
479
- scale=3,
480
- )
481
  with gr.Column(scale=1):
482
- gr.Markdown(GEMINI_API_HELP)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
  with gr.Row():
485
  model_dropdown = gr.Dropdown(
@@ -490,9 +783,9 @@ def create_ui():
490
  )
491
 
492
  # Repository Analysis Tab
493
- with gr.Tab("Analisis Repository"):
494
  with gr.Group():
495
- # Repository URL and Token inputs
496
  with gr.Row():
497
  repo_url = gr.Textbox(
498
  label="URL Repository GitHub",
@@ -500,15 +793,35 @@ def create_ui():
500
  elem_classes="mobile-full",
501
  )
502
 
 
503
  with gr.Row():
504
  with gr.Column(scale=2):
505
  github_token = gr.Textbox(
506
- label="Token GitHub (opsional)",
507
  type="password",
508
- placeholder="Masukkan github token jika repo private",
509
  elem_classes="mobile-full",
510
  )
511
- gr.Markdown(GITHUB_TOKEN_HELP)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  with gr.Column(scale=1):
513
  branch = gr.Textbox(
514
  label="Branch (opsional)",
@@ -516,16 +829,14 @@ def create_ui():
516
  elem_classes="mobile-full",
517
  )
518
 
519
-
520
  clone_button = gr.Button(
521
- "Analisa Repo",
522
  variant="primary",
523
  elem_classes="mobile-full",
524
  )
525
 
526
- clone_status = gr.Markdown(
527
- value="", label="Status Repository", elem_classes="mobile-full"
528
- )
529
 
530
  # File Selection
531
  with gr.Group():
@@ -547,17 +858,200 @@ def create_ui():
547
  )
548
 
549
  # Examples Tab
550
- with gr.Tab("Ide Cepat"):
551
- example_output = gr.HTML(
552
- value="Pilih file di tab Analisis Repository untuk melihat contoh pertanyaan.",
553
- label="Contoh Pertanyaan",
554
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
  # Chat Interface (outside tabs)
557
  with gr.Group():
558
  chat_history = gr.Chatbot(
559
- label="Open Repo AI Assistant",
560
- height=300,
561
  show_label=True,
562
  type="messages",
563
  elem_classes="mobile-full",
@@ -565,19 +1059,20 @@ def create_ui():
565
 
566
  with gr.Row():
567
  chat_input = gr.Textbox(
568
- label="Chat Dengan Repository",
569
- placeholder="Ketik di sini...",
570
  lines=3,
571
  elem_classes="mobile-full",
572
  )
573
- send_button = gr.Button("Kirim", variant="primary")
574
- clear_button = gr.Button("Bersihkan", variant="secondary")
 
575
 
576
  # Event Handlers
577
  def handle_clone(repo_url, github_token, branch):
578
  if not repo_url:
579
  return (
580
- "URL repository diperlukan!",
581
  gr.Dropdown(choices=[]),
582
  "<div class='file-list'>Belum ada file yang dipilih</div>",
583
  )
@@ -608,102 +1103,43 @@ def create_ui():
608
  html += "</div>"
609
  return html
610
 
611
- def generate_examples(selected_files):
612
- if not selected_files:
613
- return """
614
- <div class='example-list'>
615
- <h3>Pilih File Terlebih Dahulu</h3>
616
- <p>Silakan pilih file di tab Analisis Repository untuk melihat contoh pertanyaan yang relevan.</p>
617
- </div>
618
- """
619
-
620
- examples = "<div class='example-list'>"
621
-
622
- # General examples for any file
623
- examples += """
624
- <h3>Contoh Pertanyaan Umum:</h3>
625
- <ul>
626
- """
627
-
628
- file_names = ", ".join(
629
- [f"<code>{f.split('/')[-1]}</code>" for f in selected_files]
630
- )
631
- examples += f"""
632
- <li><strong>Analisis Kode:</strong> "Jelaskan logika dan fungsi utama dari {file_names}"</li>
633
- <li><strong>Deteksi Bug:</strong> "Apakah ada potensi bug atau masalah keamanan di file-file ini?"</li>
634
- <li><strong>Best Practices:</strong> "Bagaimana cara mengoptimalkan kode di file-file ini?"</li>
635
- """
636
-
637
- # Specific examples based on file types
638
- for file in selected_files:
639
- filename = file.split("/")[-1]
640
- ext = filename.split(".")[-1].lower() if "." in filename else ""
641
- examples += f"<h4>Contoh untuk {filename}:</h4><ul>"
642
-
643
- if ext in ["py", "js", "java", "cpp", "c", "go"]:
644
- examples += f"""
645
- <li>"Jelaskan fungsi-fungsi utama di {filename}"</li>
646
- <li>"Bagaimana cara mengoptimalkan performa di {filename}?"</li>
647
- <li>"Buat unit test untuk fungsi-fungsi di {filename}"</li>
648
- """
649
- elif ext in ["html", "css"]:
650
- examples += f"""
651
- <li>"Analisis struktur dan layout dari {filename}"</li>
652
- <li>"Bagaimana cara membuat {filename} lebih responsif?"</li>
653
- <li>"Optimasi untuk mobile view di {filename}"</li>
654
- """
655
- elif ext == "md":
656
- examples += f"""
657
- <li>"Ringkas isi dokumentasi dari {filename}"</li>
658
- <li>"Buat tabel konten untuk {filename}"</li>
659
- <li>"Perbaiki formatting di {filename}"</li>
660
- """
661
- elif ext in ["json", "yaml", "yml"]:
662
- examples += f"""
663
- <li>"Validasi struktur data di {filename}"</li>
664
- <li>"Jelaskan konfigurasi di {filename}"</li>
665
- <li>"Optimasi format di {filename}"</li>
666
- """
667
- elif ext == "dockerfile":
668
- examples += f"""
669
- <li>"Analisis keamanan dari {filename}"</li>
670
- <li>"Optimasi multi-stage build di {filename}"</li>
671
- <li>"Best practices untuk {filename}"</li>
672
- """
673
- else:
674
- examples += f"""
675
- <li>"Analisis isi dari {filename}"</li>
676
- <li>"Jelaskan struktur dan tujuan {filename}"</li>
677
- <li>"Saran perbaikan untuk {filename}"</li>
678
- """
679
- examples += "</ul>"
680
-
681
- examples += """
682
- <h3>Tips:</h3>
683
- <ul>
684
- <li>Gunakan pertanyaan yang spesifik dan fokus pada bagian tertentu</li>
685
- <li>Sebutkan nama file jika bertanya tentang file tertentu</li>
686
- <li>Jelaskan konteks atau masalah yang ingin diselesaikan</li>
687
- </ul>
688
- """
689
-
690
- examples += "</div>"
691
- return examples
692
-
693
  def clear_chat_history():
694
  return []
695
 
696
- def update_model_list(provider_choice):
697
- if provider_choice == AIProvider.XAI:
698
- return gr.Dropdown(choices=XAI_MODELS, value="grok-2-latest")
699
- elif provider_choice == AIProvider.GEMINI:
700
- return gr.Dropdown(choices=GEMINI_MODELS, value="gemini-1.5-mini")
701
- else: # OLLAMA
702
- return gr.Dropdown(choices=OLLAMA_MODELS, value="llama2")
 
 
 
 
 
 
 
 
 
 
 
 
 
703
 
704
  # Connect Events
705
- provider.change(
706
- fn=update_model_list, inputs=[provider], outputs=[model_dropdown]
 
 
 
 
 
 
 
 
 
 
707
  )
708
 
709
  clone_button.click(
@@ -712,25 +1148,26 @@ def create_ui():
712
  outputs=[clone_status, file_selector, file_list],
713
  )
714
 
715
- file_selector.change(
716
- fn=update_file_list, inputs=[file_selector], outputs=[file_list]
717
- )
718
-
719
- # Example tab updates
720
- tabs.select(
721
- fn=generate_examples,
722
- inputs=[file_selector],
723
- outputs=[example_output],
724
- api_name=False,
725
- )
726
 
727
- # Also update when file selection changes
728
- file_selector.change(
729
- fn=generate_examples, inputs=[file_selector], outputs=[example_output]
730
- )
731
 
732
  # Chat events
733
- clear_button.click(fn=clear_chat_history, outputs=[chat_history])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
735
  send_button.click(
736
  fn=handle_chat,
@@ -766,9 +1203,10 @@ def create_ui():
766
 
767
 
768
  if __name__ == "__main__":
769
- print(f"""
770
- 🚀 Memulai Repository Chat Analysis
 
771
  """)
772
 
773
  app = create_ui()
774
- app.launch(share=True)
 
16
  # Load environment variables
17
  dotenv.load_dotenv()
18
 
19
+ # Metadata# Metadata
20
+ CURRENT_TIME = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
21
+ CURRENT_USER = os.getenv("USER", "ErRickow")
22
 
23
  # Default API Keys (fallback if user doesn't provide their own)
24
  DEFAULT_XAI_KEY = os.getenv(
25
+ "XAI_API_KEY",
26
+ "xai-vfjhklL384Z4HKdItsZomqpFlXubTZJAFnISQUpV7dE8lRnWwYBVPSCxSTlu08wDbAcv720bx2dDiQ9x",
27
  )
28
  DEFAULT_GEMINI_KEY = os.getenv("GEMINI_API_KEY")
29
+ GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
30
 
31
  # API settings
32
  OLLAMA_API = os.environ.get("OLLAMA_API", "http://localhost:11434")
 
115
  """
116
 
117
 
118
+ async def get_available_models(provider: str, api_key: str = None) -> List[str]:
119
+ """Mendapatkan daftar model yang tersedia dari API."""
120
+ try:
121
+ if provider == AIProvider.XAI:
122
+ if not api_key and not DEFAULT_XAI_KEY:
123
+ return ["grok-2-latest"], "⚠️ API Key diperlukan untuk mendapatkan daftar model lengkap"
124
+
125
+ client = AsyncOpenAI(api_key=api_key or DEFAULT_XAI_KEY, base_url=XAI_BASE_URL)
126
+ models = await client.models.list()
127
+ available_models = [m.id for m in models.data if "grok" in m.id.lower()]
128
+ return available_models, None
129
+
130
+ elif provider == AIProvider.GEMINI:
131
+ if not api_key and not DEFAULT_GEMINI_KEY:
132
+ return ["gemini-pro"], "⚠️ API Key diperlukan untuk mendapatkan daftar model lengkap"
133
+
134
+ genai.configure(api_key=api_key or DEFAULT_GEMINI_KEY)
135
+ models = [m.name for m in genai.list_models() if "gemini" in m.name.lower()]
136
+ return models, None
137
+
138
+ else: # OLLAMA
139
+ try:
140
+ response = requests.get(f"{OLLAMA_API}/api/tags")
141
+ if response.status_code == 200:
142
+ models = [m["name"] for m in response.json()["models"]]
143
+ return models, None
144
+ return ["llama2"], f"⚠️ Error mengakses Ollama API: {response.status_code}"
145
+ except Exception as e:
146
+ return ["llama2"], f"⚠️ Error connecting to Ollama: {str(e)}"
147
+
148
+ except Exception as e:
149
+ return [], f"⚠️ Error mendapatkan daftar model: {str(e)}"
150
+
151
+
152
  class AIProvider:
153
  OLLAMA = "ollama"
154
  GEMINI = "gemini"
 
161
  self.repo_content = {}
162
  self.chat_history = []
163
 
164
+ async def stream_gemini_response(
165
+ self, prompt: str, api_key: str = None, model: str = "gemini-pro"
166
  ) -> AsyncGenerator[str, None]:
167
+ """Stream response dari Gemini API menggunakan OpenAI client"""
168
  try:
169
+ actual_key = api_key if api_key else DEFAULT_GEMINI_KEY
 
 
170
  if not actual_key:
171
+ yield "⚠️ API Key Gemini diperlukan. Klik icon bantuan (?) untuk panduan mendapatkan key."
172
  return
173
 
174
+ # Gunakan OpenAI client untuk Gemini
175
+ client = AsyncOpenAI(api_key=actual_key, base_url=GEMINI_BASE_URL)
176
 
177
+ # Tambahkan konteks repository jika ada
178
  messages = [
179
  {
180
  "role": "system",
 
186
  context = f"Repository: {self.current_repo}\n\n"
187
  repo_files = "\n".join(list(self.repo_content.keys()))
188
  context += f"Files in repository:\n{repo_files}\n\n"
189
+ prompt = context + prompt
190
 
191
  messages.append({"role": "user", "content": prompt})
192
 
193
+ try:
194
+ stream = await client.chat.completions.create(
195
+ model=model,
196
+ messages=messages,
197
+ stream=True,
198
+ temperature=0.7,
199
+ top_p=0.8,
200
+ max_tokens=4096,
201
+ )
202
 
203
+ async for chunk in stream:
204
+ if chunk.choices[0].delta.content:
205
+ yield chunk.choices[0].delta.content
 
 
 
206
 
207
+ except Exception as e:
208
+ if "model not found" in str(e).lower():
209
+ yield f"⚠️ Model {model} tidak tersedia di Gemini API"
210
+ elif "rate limit" in str(e).lower():
211
+ yield "⚠️ Rate limit tercapai. Coba lagi nanti atau gunakan API key yang berbeda."
212
+ else:
213
+ yield f"⚠️ Error saat streaming dari Gemini: {str(e)}"
214
+ return
215
 
216
  except Exception as e:
217
+ error_msg = f"⚠️ Error dalam Gemini API: {str(e)}\n\nPastikan:\n1. API Key valid\n2. Model {model} tersedia\n3. Anda memiliki kuota yang cukup"
218
  print(error_msg)
219
  yield error_msg
220
+
221
+ async def stream_xai_response(
222
+ self, prompt: str, api_key: str = None, model: str = "grok-2-latest"
223
  ) -> AsyncGenerator[str, None]:
224
+ """Stream response dari X.AI (Grok) API dengan support berbagai model"""
225
  try:
226
+ actual_key = api_key if api_key else DEFAULT_XAI_KEY
227
+ if not actual_key:
228
+ yield "⚠️ API Key X.AI diperlukan. Gunakan key Anda sendiri atau tunggu reset limit default key."
229
  return
230
+
231
+ client = AsyncOpenAI(api_key=actual_key, base_url=XAI_BASE_URL)
232
+
233
+ # Verifikasi model support
234
+ try:
235
+ model_info = await client.models.retrieve(model)
236
+ if not any(c.type == "chat" for c in model_info.capabilities):
237
+ yield f"⚠️ Model {model} tidak mendukung chat completion"
238
+ return
239
+ except Exception as e:
240
+ yield f"⚠️ Error verifikasi model {model}: {str(e)}"
241
+ return
242
+
243
+ try:
244
+ stream = await client.chat.completions.create(
245
+ model=model,
246
+ messages=[
247
+ {
248
+ "role": "system",
249
+ "content": "Anda adalah asisten AI yang membantu menganalisis repository code. Berikan respons dalam Bahasa Indonesia.",
250
+ },
251
+ {"role": "user", "content": prompt},
252
+ ],
253
+ stream=True,
254
+ )
255
+ except Exception as e:
256
+ yield f"⚠️ Error streaming dari model {model}: {str(e)}"
257
+ return
258
+
259
+ async for chunk in stream:
260
+ if chunk.choices[0].delta.content:
261
+ yield chunk.choices[0].delta.content
262
+
263
  except Exception as e:
264
+ yield f"⚠️ Error dalam X.AI API: {str(e)}\nPastikan:\n1. API Key valid\n2. Model {model} tersedia\n3. Anda memiliki akses ke model ini"
 
 
265
 
266
+ def clone_repository(self, repo_url: str, github_token: str, branch: str = None) -> tuple[bool, str]:
 
 
267
  """Clone repository GitHub dengan autentikasi"""
268
  if not repo_url:
269
  return False, "⚠️ URL repository diperlukan"
 
278
 
279
  # Cek apakah repository private
280
  headers = {"Authorization": f"token {github_token}"} if github_token else {}
281
+ repo_check = requests.get(f"https://api.github.com/repos/{owner_repo}", headers=headers)
 
 
282
 
283
  if repo_check.status_code == 404:
284
  return False, "⚠️ Repository tidak ditemukan. Periksa URL repository."
 
287
  False,
288
  "⚠️ Token GitHub tidak valid. Klik icon bantuan (?) untuk panduan mendapatkan token.",
289
  )
290
+ elif repo_check.status_code == 403 and repo_check.json().get("private", False):
 
 
291
  return (
292
  False,
293
  "⚠️ Ini adalah repository private. Token GitHub dengan akses 'repo' diperlukan.",
 
344
  continue
345
  return False, "Tidak dapat membaca file dengan encoding yang didukung"
346
 
347
+
348
  analyzer = RepoAnalyzer()
349
 
350
+
351
  async def handle_chat(
352
  message,
353
  history,
 
359
  analyzer=analyzer,
360
  ):
361
  """Menangani interaksi chat dengan model AI"""
362
+ try:
363
+ if not analyzer.current_repo:
364
+ new_message = {
365
+ "role": "assistant",
366
+ "content": "⚠️ Mohon clone repository terlebih dahulu sebelum mengajukan pertanyaan.",
367
+ }
368
+ history = history or []
369
+ history.append({"role": "user", "content": message})
370
+ history.append(new_message)
371
+ yield history
372
+ return
373
 
374
+ history = history or []
375
+ user_message = f'<div class="user-message">{message}</div>'
376
+ history.append({"role": "user", "content": user_message})
377
+ history.append({"role": "assistant", "content": '<div class="assistant-message">'})
378
 
379
+ # Add context about selected files
 
380
  file_context = ""
381
  if selected_files:
382
  file_context = "\n\nFile yang dipilih:\n"
383
  for file in selected_files:
384
  content = analyzer.repo_content.get(file, "")
385
+ if content:
386
  file_context += f"\n{file}:\n```\n{content}\n```\n"
387
 
388
  enhanced_message = f"{message}\n{file_context}"
 
389
  full_response = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
+ try:
392
+ if provider_choice == AIProvider.XAI:
393
+ async for chunk in analyzer.stream_xai_response(enhanced_message, xai_key, model_name):
394
+ full_response += chunk
395
+ formatted_response = format_response(full_response)
396
+ history[-1]["content"] = f'<div class="assistant-message">{formatted_response}</div>'
397
+ await asyncio.sleep(0.05)
398
+ yield history
399
+
400
+ elif provider_choice == AIProvider.GEMINI:
401
+ if not gemini_key and not DEFAULT_GEMINI_KEY:
402
+ error_msg = "⚠️ API Key Gemini diperlukan"
403
+ history[-1]["content"] = f'<div class="assistant-message">{error_msg}</div>'
404
+ yield history
405
+ return
406
+
407
+ async for chunk in analyzer.stream_gemini_response(enhanced_message, gemini_key or DEFAULT_GEMINI_KEY):
408
+ full_response += chunk
409
+ formatted_response = format_response(full_response)
410
+ history[-1]["content"] = f'<div class="assistant-message">{formatted_response}</div>'
411
+ await asyncio.sleep(0.05)
412
+ yield history
413
+
414
+ else: # OLLAMA
415
+ try:
416
+ response = analyze_with_ollama(model_name, enhanced_message)
417
+ words = response.split()
418
+ for i in range(len(words)):
419
+ full_response = " ".join(words[: i + 1])
420
+ formatted_response = format_response(full_response)
421
+ history[-1]["content"] = f'<div class="assistant-message">{formatted_response}</div>'
422
+ await asyncio.sleep(0.05)
423
+ yield history
424
+ except Exception as e:
425
+ error_msg = f"⚠️ Error dengan Ollama: {str(e)}\nPastikan:\n1. Ollama berjalan di {OLLAMA_API}\n2. Model {model_name} sudah diinstall"
426
+ history[-1]["content"] = f'<div class="assistant-message">{error_msg}</div>'
427
+ yield history
428
+
429
+ except Exception as e:
430
+ error_msg = f"⚠️ Error saat streaming response: {str(e)}"
431
+ history[-1]["content"] = f'<div class="assistant-message">{error_msg}</div>'
432
+ yield history
433
 
434
  except Exception as e:
435
+ error_msg = f"⚠️ Error umum: {str(e)}"
436
+ history[-1]["content"] = f'<div class="assistant-message">{error_msg}</div>'
437
  yield history
438
 
439
 
440
+ def format_response(text):
441
+ """Format response text with code blocks"""
442
+ import re
443
+ import uuid
444
+
445
+ # Replace code blocks with styled versions
446
+ def replace_code_block(match):
447
+ language = match.group(1) or ""
448
+ code = match.group(2)
449
+ block_id = f"code-block-{uuid.uuid4().hex[:8]}"
450
+
451
+ return f'''
452
+ <div class="code-block">
453
+ <div class="code-header">
454
+ <span class="code-title">{language}</span>
455
+ <button class="code-copy-btn" id="{block_id}-btn" onclick="copyCode('{block_id}')">Copy</button>
456
+ </div>
457
+ <pre class="code-content" id="{block_id}">{code}</pre>
458
+ </div>
459
+ '''
460
+
461
+ # Replace ```language\ncode``` blocks
462
+ text = re.sub(r"```(\w+)?\n(.*?)```", replace_code_block, text, flags=re.DOTALL)
463
+
464
+ # Replace inline code
465
+ text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
466
+
467
+ return text
468
+
469
+
470
+ def clear_all():
471
+ """Clear semua input dan status"""
472
+ return (
473
+ "", # repo_url
474
+ "", # github_token
475
+ "", # branch
476
+ "", # clone_status
477
+ [], # chat_history
478
+ [], # file_selector
479
+ "<div class='file-list'>Belum ada file yang dipilih</div>", # file_list
480
+ "", # xai_key
481
+ "", # gemini_key
482
+ "grok-2-latest", # model_dropdown default value
483
+ )
484
+
485
+
486
  def create_ui():
 
487
  global analyzer
488
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
489
 
490
  with gr.Blocks(title="Open Repo AI", theme=gr.themes.Soft()) as app:
491
  # CSS Styling
492
  gr.Markdown("""
493
  <style>
494
+ /* General Styles */
495
  .container { max-width: 100% !important; padding: 1rem; }
496
  .mobile-full { width: 100% !important; }
497
+
498
+ /* Header Styles */
499
+ .header-info {
500
+ display: flex;
501
+ justify-content: space-between;
502
+ padding: 10px;
503
  background: #f8f9fa;
504
+ border-radius: 4px;
 
505
  margin: 10px 0;
506
+ font-family: monospace;
507
  }
508
+ .timestamp, .user-info {
509
+ padding: 5px 10px;
510
+ background: #fff;
511
+ border-radius: 4px;
512
+ border: 1px solid #e9ecef;
513
+ }
514
+
515
+ /* File List Styles */
516
+ .file-list {
517
+ margin: 10px 0;
518
+ padding: 10px;
519
+ border: 1px solid #ddd;
520
+ border-radius: 4px;
521
  }
522
+ .file-item {
523
+ display: flex;
524
+ justify-content: space-between;
525
+ padding: 5px 0;
 
526
  }
527
+
528
+ /* Collapsible Sections */
529
+ details {
530
  margin: 10px 0;
531
+ border: 1px solid #e9ecef;
532
+ border-radius: 8px;
533
+ overflow: hidden;
534
  }
535
+ summary {
536
+ padding: 15px;
537
+ background: #f8f9fa;
538
+ cursor: pointer;
539
+ font-weight: 600;
540
+ display: flex;
541
+ align-items: center;
542
+ transition: background-color 0.3s;
543
  }
544
+ summary:hover {
545
  background: #e9ecef;
546
+ }
547
+ .help-content {
548
+ padding: 15px;
549
+ background: white;
550
+ }
551
+
552
+ /* Example Sections */
553
+ .examples-wrapper {
554
+ margin: 20px 0;
555
+ }
556
+ .example-section {
557
+ margin: 10px 0;
558
+ }
559
+ .example-content {
560
+ padding: 15px;
561
+ background: white;
562
+ }
563
+ .example-btn {
564
+ width: 100%;
565
+ text-align: left;
566
+ padding: 8px 12px;
567
+ background: #f8f9fa;
568
+ border: 1px solid #e9ecef;
569
  border-radius: 4px;
570
+ cursor: pointer;
571
+ transition: all 0.3s;
572
  }
573
+ .example-btn:hover {
574
+ background: #e9ecef;
575
+ border-color: #dee2e6;
576
+ }
577
+
578
+ /* Chat Message Styling */
579
+ .message-wrap {
580
+ display: flex;
581
+ flex-direction: column;
582
+ gap: 1rem;
583
+ }
584
+ .user-message, .assistant-message {
585
+ padding: 1rem;
586
+ border-radius: 8px;
587
+ max-width: 90%;
588
+ }
589
+ .user-message {
590
+ align-self: flex-end;
591
+ background: #007AFF;
592
+ color: white;
593
+ }
594
+ .assistant-message {
595
+ align-self: flex-start;
596
+ background: #f8f9fa;
597
+ border: 1px solid #e9ecef;
598
+ }
599
+
600
+ /* Code Block Styling */
601
+ .code-block {
602
+ position: relative;
603
+ margin: 1rem 0;
604
+ border-radius: 8px;
605
+ overflow: hidden;
606
+ border: 1px solid #e9ecef;
607
+ }
608
+ .code-header {
609
+ display: flex;
610
+ justify-content: space-between;
611
+ align-items: center;
612
+ padding: 0.5rem 1rem;
613
+ background: #f8f9fa;
614
+ border-bottom: 1px solid #e9ecef;
615
+ }
616
+ .code-title {
617
+ font-family: monospace;
618
  font-weight: bold;
619
+ color: #495057;
620
  }
621
+ .code-copy-btn {
622
+ padding: 4px 8px;
623
+ font-size: 12px;
624
+ color: #6c757d;
625
+ background: white;
626
+ border: 1px solid #ced4da;
627
+ border-radius: 4px;
628
+ cursor: pointer;
629
+ transition: all 0.2s;
630
+ }
631
+ .code-copy-btn:hover {
632
+ background: #e9ecef;
633
+ border-color: #adb5bd;
634
  }
635
+ .code-content {
636
+ padding: 1rem;
637
+ background: #282a36;
638
+ color: #f8f8f2;
639
+ overflow-x: auto;
640
+ font-family: monospace;
641
+ }
642
+
643
+ /* Media Queries */
644
  @media (max-width: 768px) {
645
  .gr-form { flex-direction: column !important; }
646
  .gr-group { margin: 0.5rem 0 !important; }
647
  }
648
+ .clear-button {
649
+ background: #dc3545 !important;
650
+ color: white !important;
651
+ border: none !important;
652
+ padding: 0.5rem 1rem !important;
653
+ border-radius: 4px !important;
654
+ cursor: pointer !important;
655
+ transition: background-color 0.2s !important;
656
+ }
657
+
658
+ .clear-button:hover {
659
+ background: #c82333 !important;
660
+ }
661
+
662
+ /* Button Container */
663
+ .button-container {
664
+ display: flex;
665
+ gap: 10px;
666
+ margin-top: 10px;
667
+ }
668
+
669
+ .button-container button {
670
+ flex: 1;
671
+ }
672
  </style>
673
+
674
+ <script>
675
+ function copyCode(blockId) {
676
+ const codeBlock = document.getElementById(blockId);
677
+ const code = codeBlock.innerText;
678
+ navigator.clipboard.writeText(code).then(() => {
679
+ const btn = document.querySelector(`#${blockId}-btn`);
680
+ btn.innerText = 'Copied!';
681
+ setTimeout(() => {
682
+ btn.innerText = 'Copy';
683
+ }, 2000);
684
+ });
685
+ }
686
+
687
+ function setQuestion(text) {
688
+ const chatInput = document.querySelector('textarea[data-testid="textbox"]');
689
+ if (chatInput) {
690
+ chatInput.value = text;
691
+ chatInput.focus();
692
+ }
693
+ }
694
+ </script>
695
  """)
696
 
697
  # Header
698
  with gr.Row(elem_classes="container"):
699
  gr.Markdown(f"""
700
+ # AI Github Repository Chat
701
 
702
+ <div class="header-info">
703
+ <span class="timestamp">Current Date and Time (UTC - YYYY-MM-DD HH:MM:SS formatted): {current_time}</span>
704
+ </div>
705
  """)
706
 
707
  # Main Tabs Container
708
  with gr.Tabs() as tabs:
709
  # Configuration Tab
710
+ with gr.Tab("🛠️ Konfigurasi"):
711
  provider = gr.Radio(
712
  choices=[AIProvider.XAI, AIProvider.GEMINI, AIProvider.OLLAMA],
713
+ label="Penyedia AI",
714
  value=AIProvider.XAI,
715
  )
716
 
717
  with gr.Group() as api_settings:
718
+ # X.AI API Key section
719
  with gr.Row():
720
+ with gr.Column(scale=3):
721
+ xai_key = gr.Textbox(
722
+ label="X.AI (Grok) API Key",
723
+ type="password",
724
+ placeholder="Opsional - Klik (?) untuk info",
725
+ show_label=True,
726
+ )
727
  with gr.Column(scale=1):
728
+ gr.Markdown("""
729
+ <details>
730
+ <summary>❓ Cara Mendapatkan X.AI API Key</summary>
731
+ <div class="help-content">
732
+ <ol>
733
+ <li>Kunjungi <a href="https://x.ai" target="_blank">X.AI Developer Portal</a></li>
734
+ <li>Daftar/Login ke akun Anda</li>
735
+ <li>Buat API Key baru</li>
736
+ <li>Salin API Key</li>
737
+ </ol>
738
+ <p><strong>Catatan:</strong></p>
739
+ <ul>
740
+ <li>Jika tidak diisi, akan menggunakan API key default</li>
741
+ <li>Masukkan API key Anda sendiri jika default mencapai limit</li>
742
+ </ul>
743
+ </div>
744
+ </details>
745
+ """)
746
+
747
+ # Gemini API Key section
748
  with gr.Row():
749
+ with gr.Column(scale=3):
750
+ gemini_key = gr.Textbox(
751
+ label="Gemini API Key",
752
+ type="password",
753
+ placeholder="Opsional - Klik (?) untuk info",
754
+ show_label=True,
755
+ )
756
  with gr.Column(scale=1):
757
+ gr.Markdown("""
758
+ <details>
759
+ <summary>❓ Cara Mendapatkan Gemini API Key</summary>
760
+ <div class="help-content">
761
+ <ol>
762
+ <li>Kunjungi <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a></li>
763
+ <li>Login dengan akun Google Anda</li>
764
+ <li>Klik "Create API Key"</li>
765
+ <li>Salin API Key yang dihasilkan</li>
766
+ </ol>
767
+ <p><strong>Catatan:</strong></p>
768
+ <ul>
769
+ <li>Gemini memberikan kuota gratis setiap bulan</li>
770
+ <li>Key bisa dibuat ulang jika diperlukan</li>
771
+ <li>Monitor penggunaan di <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a></li>
772
+ </ul>
773
+ </div>
774
+ </details>
775
+ """)
776
 
777
  with gr.Row():
778
  model_dropdown = gr.Dropdown(
 
783
  )
784
 
785
  # Repository Analysis Tab
786
+ with gr.Tab("📊 Analisis Repository"):
787
  with gr.Group():
788
+ # Repository URL input
789
  with gr.Row():
790
  repo_url = gr.Textbox(
791
  label="URL Repository GitHub",
 
793
  elem_classes="mobile-full",
794
  )
795
 
796
+ # GitHub Token section
797
  with gr.Row():
798
  with gr.Column(scale=2):
799
  github_token = gr.Textbox(
800
+ label="Token GitHub",
801
  type="password",
802
+ placeholder="Klik (?) untuk panduan",
803
  elem_classes="mobile-full",
804
  )
805
+ gr.Markdown("""
806
+ <details>
807
+ <summary>❓ Cara Mendapatkan GitHub Token</summary>
808
+ <div class="help-content">
809
+ <ol>
810
+ <li>Kunjungi <a href="https://github.com/settings/tokens" target="_blank">GitHub Token Settings</a></li>
811
+ <li>Klik "Generate new token" > "Generate new token (classic)"</li>
812
+ <li>Beri nama token Anda di "Note"</li>
813
+ <li>Pilih scope:
814
+ <ul>
815
+ <li><code>repo</code> (untuk akses repository private)</li>
816
+ <li><code>read:packages</code> (opsional, untuk akses package)</li>
817
+ </ul>
818
+ </li>
819
+ <li>Klik "Generate token"</li>
820
+ <li><strong>PENTING:</strong> Salin token segera! Token hanya ditampilkan sekali</li>
821
+ </ol>
822
+ </div>
823
+ </details>
824
+ """)
825
  with gr.Column(scale=1):
826
  branch = gr.Textbox(
827
  label="Branch (opsional)",
 
829
  elem_classes="mobile-full",
830
  )
831
 
832
+ # Clone Button and Status
833
  clone_button = gr.Button(
834
+ "🔄 Clone Repository",
835
  variant="primary",
836
  elem_classes="mobile-full",
837
  )
838
 
839
+ clone_status = gr.Markdown(value="", label="Status Repository", elem_classes="mobile-full")
 
 
840
 
841
  # File Selection
842
  with gr.Group():
 
858
  )
859
 
860
  # Examples Tab
861
+ with gr.Tab("💡 Examples"):
862
+ gr.Markdown("""
863
+ <style>
864
+ /* Styling untuk collapse */
865
+ .examples-container {
866
+ padding: 20px;
867
+ background: #fff;
868
+ border-radius: 8px;
869
+ }
870
+
871
+ .category-collapse {
872
+ margin-bottom: 15px;
873
+ border: 1px solid #e9ecef;
874
+ border-radius: 8px;
875
+ overflow: hidden;
876
+ }
877
+
878
+ .category-header {
879
+ padding: 15px;
880
+ background: #f8f9fa;
881
+ cursor: pointer;
882
+ user-select: none;
883
+ display: flex;
884
+ align-items: center;
885
+ justify-content: space-between;
886
+ transition: background-color 0.2s;
887
+ }
888
+
889
+ .category-header:hover {
890
+ background: #e9ecef;
891
+ }
892
+
893
+ .category-content {
894
+ padding: 15px;
895
+ display: none;
896
+ border-top: 1px solid #e9ecef;
897
+ }
898
+
899
+ .category-collapse.active .category-content {
900
+ display: block;
901
+ animation: slideDown 0.3s ease-out;
902
+ }
903
+
904
+ .category-header::after {
905
+ content: '▼';
906
+ font-size: 12px;
907
+ transition: transform 0.3s;
908
+ }
909
+
910
+ .category-collapse.active .category-header::after {
911
+ transform: rotate(180deg);
912
+ }
913
+
914
+ .example-button {
915
+ display: block;
916
+ width: 100%;
917
+ padding: 10px;
918
+ margin: 5px 0;
919
+ text-align: left;
920
+ background: #fff;
921
+ border: 1px solid #e9ecef;
922
+ border-radius: 4px;
923
+ cursor: pointer;
924
+ transition: all 0.2s;
925
+ }
926
+
927
+ .example-button:hover {
928
+ background: #f8f9fa;
929
+ border-color: #dee2e6;
930
+ }
931
+
932
+ @keyframes slideDown {
933
+ from {
934
+ opacity: 0;
935
+ transform: translateY(-10px);
936
+ }
937
+ to {
938
+ opacity: 1;
939
+ transform: translateY(0);
940
+ }
941
+ }
942
+ </style>
943
+
944
+ <div class="examples-container">
945
+ <h3>🎯 Contoh Pertanyaan</h3>
946
+ <p>Klik pada kategori untuk melihat contoh pertanyaan yang bisa diajukan</p>
947
+
948
+ <div class="category-collapse">
949
+ <div class="category-header">
950
+ 🔍 Analisis Kode
951
+ </div>
952
+ <div class="category-content">
953
+ <button class="example-button" onclick="setQuestion('Jelaskan logika dan alur dari kode ini')">
954
+ Jelaskan logika dari kode
955
+ </button>
956
+ <button class="example-button" onclick="setQuestion('Bagaimana cara mengoptimalkan performa kode ini?')">
957
+ Optimasi performa
958
+ </button>
959
+ <button class="example-button" onclick="setQuestion('Apakah ada potential bugs yang perlu diperbaiki?')">
960
+ Cek potential bugs
961
+ </button>
962
+ </div>
963
+ </div>
964
+
965
+ <div class="category-collapse">
966
+ <div class="category-header">
967
+ 📚 Best Practices
968
+ </div>
969
+ <div class="category-content">
970
+ <button class="example-button" onclick="setQuestion('Apa saja best practices yang bisa diterapkan di kode ini?')">
971
+ Saran best practices
972
+ </button>
973
+ <button class="example-button" onclick="setQuestion('Bagaimana cara meningkatkan maintainability kode ini?')">
974
+ Improve maintainability
975
+ </button>
976
+ <button class="example-button" onclick="setQuestion('Berikan saran untuk meningkatkan code quality')">
977
+ Improve code quality
978
+ </button>
979
+ </div>
980
+ </div>
981
+
982
+ <div class="category-collapse">
983
+ <div class="category-header">
984
+ 🧪 Testing
985
+ </div>
986
+ <div class="category-content">
987
+ <button class="example-button" onclick="setQuestion('Buatkan unit test untuk fungsi ini')">
988
+ Generate unit tests
989
+ </button>
990
+ <button class="example-button" onclick="setQuestion('Bagaimana cara test error handling yang baik?')">
991
+ Test error handling
992
+ </button>
993
+ <button class="example-button" onclick="setQuestion('Berikan saran untuk meningkatkan test coverage')">
994
+ Improve test coverage
995
+ </button>
996
+ </div>
997
+ </div>
998
+
999
+ <div class="category-collapse">
1000
+ <div class="category-header">
1001
+ 📝 Dokumentasi
1002
+ </div>
1003
+ <div class="category-content">
1004
+ <button class="example-button" onclick="setQuestion('Buatkan dokumentasi untuk fungsi-fungsi utama')">
1005
+ Generate documentation
1006
+ </button>
1007
+ <button class="example-button" onclick="setQuestion('Buat README.md untuk project ini')">
1008
+ Create README.md
1009
+ </button>
1010
+ <button class="example-button" onclick="setQuestion('Jelaskan cara penggunaan library/framework')">
1011
+ Usage guide
1012
+ </button>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+
1017
+ <script>
1018
+ document.addEventListener('DOMContentLoaded', function() {
1019
+ // Handle collapse functionality
1020
+ const categories = document.querySelectorAll('.category-collapse');
1021
+
1022
+ categories.forEach(category => {
1023
+ const header = category.querySelector('.category-header');
1024
+
1025
+ header.addEventListener('click', () => {
1026
+ const isActive = category.classList.contains('active');
1027
+
1028
+ // Close all categories
1029
+ categories.forEach(c => c.classList.remove('active'));
1030
+
1031
+ // If clicked category wasn't active, open it
1032
+ if (!isActive) {
1033
+ category.classList.add('active');
1034
+ }
1035
+ });
1036
+ });
1037
+ });
1038
+
1039
+ // Function to set question in chat input
1040
+ function setQuestion(text) {
1041
+ const chatInput = document.querySelector('textarea[data-testid="textbox"]');
1042
+ if (chatInput) {
1043
+ chatInput.value = text;
1044
+ chatInput.focus();
1045
+ }
1046
+ }
1047
+ </script>
1048
+ """)
1049
 
1050
  # Chat Interface (outside tabs)
1051
  with gr.Group():
1052
  chat_history = gr.Chatbot(
1053
+ label="📝 Riwayat Chat",
1054
+ height=500,
1055
  show_label=True,
1056
  type="messages",
1057
  elem_classes="mobile-full",
 
1059
 
1060
  with gr.Row():
1061
  chat_input = gr.Textbox(
1062
+ label="💭 Tanyakan tentang Repository",
1063
+ placeholder="Ketik pertanyaan Anda di sini...",
1064
  lines=3,
1065
  elem_classes="mobile-full",
1066
  )
1067
+ with gr.Column(scale=1):
1068
+ send_button = gr.Button("📤 Kirim", variant="primary")
1069
+ clear_button = gr.Button("🧹 Bersihkan Semua", variant="secondary")
1070
 
1071
  # Event Handlers
1072
  def handle_clone(repo_url, github_token, branch):
1073
  if not repo_url:
1074
  return (
1075
+ "⚠️ URL repository diperlukan!",
1076
  gr.Dropdown(choices=[]),
1077
  "<div class='file-list'>Belum ada file yang dipilih</div>",
1078
  )
 
1103
  html += "</div>"
1104
  return html
1105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
  def clear_chat_history():
1107
  return []
1108
 
1109
+ def update_model_list(provider_choice, api_key=None):
1110
+ try:
1111
+ models, error = asyncio.run(get_available_models(provider_choice, api_key))
1112
+ if error:
1113
+ return gr.Dropdown(
1114
+ choices=models,
1115
+ value=models[0] if models else None,
1116
+ label=f"Model AI ({error})",
1117
+ )
1118
+ return gr.Dropdown(
1119
+ choices=models,
1120
+ value=models[0] if models else None,
1121
+ label="Model AI",
1122
+ )
1123
+ except Exception as e:
1124
+ return gr.Dropdown(
1125
+ choices=["grok-2-latest"] if provider_choice == AIProvider.XAI else ["gemini-pro"],
1126
+ value="grok-2-latest" if provider_choice == AIProvider.XAI else "gemini-pro",
1127
+ label=f"Model AI (Error: {str(e)})",
1128
+ )
1129
 
1130
  # Connect Events
1131
+ provider.change(fn=update_model_list, inputs=[provider, xai_key], outputs=[model_dropdown])
1132
+
1133
+ xai_key.change(
1134
+ fn=lambda p, k: update_model_list(p, k) if p == AIProvider.XAI else None,
1135
+ inputs=[provider, xai_key],
1136
+ outputs=[model_dropdown],
1137
+ )
1138
+
1139
+ gemini_key.change(
1140
+ fn=lambda p, k: update_model_list(p, k) if p == AIProvider.GEMINI else None,
1141
+ inputs=[provider, gemini_key],
1142
+ outputs=[model_dropdown],
1143
  )
1144
 
1145
  clone_button.click(
 
1148
  outputs=[clone_status, file_selector, file_list],
1149
  )
1150
 
1151
+ file_selector.change(fn=update_file_list, inputs=[file_selector], outputs=[file_list])
 
 
 
 
 
 
 
 
 
 
1152
 
1153
+ clear_button = gr.Button("🧹 Bersihkan Semua", variant="secondary")
 
 
 
1154
 
1155
  # Chat events
1156
+ clear_button.click(
1157
+ fn=clear_all,
1158
+ outputs=[
1159
+ repo_url,
1160
+ github_token,
1161
+ branch,
1162
+ clone_status,
1163
+ chat_history,
1164
+ file_selector,
1165
+ file_list,
1166
+ xai_key,
1167
+ gemini_key,
1168
+ model_dropdown,
1169
+ ],
1170
+ )
1171
 
1172
  send_button.click(
1173
  fn=handle_chat,
 
1203
 
1204
 
1205
  if __name__ == "__main__":
1206
+ print("""
1207
+ 🚀 Starting Repository Chat Analysis
1208
+ ====================================
1209
  """)
1210
 
1211
  app = create_ui()
1212
+ app.launch(share=True, server_name="0.0.0.0", server_port=7860, debug=True)