ErNewdev0 commited on
Commit
3ee4379
Β·
verified Β·
1 Parent(s): eb48ce9

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +957 -0
app.py ADDED
@@ -0,0 +1,957 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+ import subprocess
6
+ import requests
7
+ import json
8
+ from datetime import datetime
9
+ import textwrap
10
+ import google.generativeai as genai
11
+ import asyncio
12
+ from typing import Generator, AsyncGenerator, List
13
+ from openai import AsyncOpenAI
14
+ import dotenv
15
+
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('XAI_API_KEY')
25
+ DEFAULT_GEMINI_KEY = os.getenv('GEMINI_API_KEY')
26
+
27
+ # API settings
28
+ OLLAMA_API = os.environ.get('OLLAMA_API', 'http://localhost:11434')
29
+ XAI_BASE_URL = 'https://api.x.ai/v1'
30
+
31
+ # Model lists
32
+ OLLAMA_MODELS = [
33
+ 'llama2',
34
+ 'codellama',
35
+ 'mistral',
36
+ 'neural-chat',
37
+ 'starling-lm',
38
+ 'dolphin-phi',
39
+ 'phi',
40
+ 'orca-mini',
41
+ ]
42
+
43
+ XAI_MODELS = [
44
+ 'grok-2-latest',
45
+ 'grok-1',
46
+ ]
47
+
48
+ GEMINI_MODELS = [
49
+ 'gemini-1.5-mini',
50
+ 'gemini-pro-vision',
51
+ ]
52
+
53
+ # Help texts
54
+
55
+ GITHUB_TOKEN_HELP = """
56
+ ### Cara Mendapatkan GitHub Token:
57
+
58
+ 1. Kunjungi [GitHub Token Settings](https://github.com/settings/tokens)
59
+ 2. Klik "Generate new token" > "Generate new token (classic)"
60
+ 3. Beri nama token Anda di "Note"
61
+ 4. Pilih scope:
62
+ - `repo` (untuk akses repository private)
63
+ - `read:packages` (opsional, untuk akses package)
64
+ 5. Klik "Generate token"
65
+ 6. **PENTING**: Salin token segera! Token hanya ditampilkan sekali
66
+
67
+ Token diperlukan untuk:
68
+ - Mengakses repository private
69
+ - Clone repository dengan rate limit lebih tinggi
70
+ - Mengakses fitur GitHub API
71
+ """
72
+
73
+ GEMINI_API_HELP = """
74
+ ### Cara Mendapatkan Gemini API Key:
75
+
76
+ 1. Kunjungi [Google AI Studio](https://makersuite.google.com/app/apikey)
77
+ 2. Login dengan akun Google Anda
78
+ 3. Klik "Create API Key"
79
+ 4. Salin API Key yang dihasilkan
80
+
81
+ Catatan:
82
+ - Gemini memberikan kuota gratis setiap bulan
83
+ - Key bisa dibuat ulang jika diperlukan
84
+ - Monitor penggunaan di [Google Cloud Console](https://console.cloud.google.com/)
85
+ """
86
+
87
+ OLLAMA_HELP = """
88
+ ### Cara Menggunakan Ollama:
89
+
90
+ 1. Install Ollama dari [ollama.ai](https://ollama.ai)
91
+ 2. Jalankan Ollama di komputer Anda
92
+ 3. Pastikan Ollama berjalan di http://localhost:11434
93
+
94
+ Catatan:
95
+ - Ollama berjalan secara lokal di komputer Anda
96
+ - Tidak memerlukan API key
97
+ - Ideal untuk privasi dan penggunaan offline
98
+ """
99
+
100
+ XAI_API_HELP = """
101
+ ### Cara Mendapatkan X.AI (Grok) API Key:
102
+
103
+ 1. Kunjungi [X.AI Developer Portal](https://x.ai)
104
+ 2. Daftar/Login ke akun Anda
105
+ 3. Buat API Key baru
106
+ 4. Salin API Key
107
+
108
+ Note:
109
+ - Jika tidak diisi, akan menggunakan API key default
110
+ - Masukkan API key Anda sendiri jika default mencapai limit
111
+ """
112
+
113
+
114
+ class AIProvider:
115
+ OLLAMA = 'ollama'
116
+ GEMINI = 'gemini'
117
+ XAI = 'xai'
118
+
119
+
120
+ class RepoAnalyzer:
121
+ def __init__(self):
122
+ self.current_repo = None
123
+ self.repo_content = {}
124
+ self.chat_history = []
125
+
126
+ async def stream_xai_response(
127
+ self, prompt: str, api_key: str = None, model: str = 'grok-2-latest'
128
+ ) -> AsyncGenerator[str, None]:
129
+ """Stream response dari X.AI (Grok) API"""
130
+ try:
131
+ # Use default key if none provided
132
+ actual_key = api_key if api_key else DEFAULT_XAI_KEY
133
+
134
+ if not actual_key:
135
+ yield '⚠️ API Key X.AI diperlukan. Gunakan key Anda sendiri atau tunggu reset limit default key.'
136
+ return
137
+
138
+ client = AsyncOpenAI(api_key=actual_key, base_url=XAI_BASE_URL)
139
+
140
+ # Prepare messages with repository context if available
141
+ messages = [
142
+ {
143
+ 'role': 'system',
144
+ 'content': 'Anda adalah asisten AI yang membantu menganalisis repository code. Berikan respons dalam Bahasa Indonesia.',
145
+ }
146
+ ]
147
+
148
+ if self.current_repo:
149
+ context = f'Repository: {self.current_repo}\n\n'
150
+ repo_files = '\n'.join(list(self.repo_content.keys()))
151
+ context += f'Files in repository:\n{repo_files}\n\n'
152
+ messages.append({'role': 'system', 'content': context})
153
+
154
+ messages.append({'role': 'user', 'content': prompt})
155
+
156
+ stream = await client.chat.completions.create(
157
+ model=model, messages=messages, stream=True
158
+ )
159
+
160
+ full_response = ''
161
+ async for chunk in stream:
162
+ if chunk.choices[0].delta.content:
163
+ content = chunk.choices[0].delta.content
164
+ full_response += content
165
+ yield content
166
+
167
+ self.chat_history.append({'role': 'user', 'content': prompt})
168
+ self.chat_history.append({'role': 'assistant', 'content': full_response})
169
+
170
+ except Exception as e:
171
+ error_msg = f'⚠️ Error dalam X.AI API: {str(e)}'
172
+ print(error_msg)
173
+ yield error_msg
174
+
175
+ async def stream_gemini_response(
176
+ self, prompt: str, api_key: str
177
+ ) -> AsyncGenerator[str, None]:
178
+ """Stream response dari Gemini API"""
179
+ try:
180
+ if not api_key:
181
+ yield '⚠️ API Key Gemini diperlukan. Klik icon bantuan (?) di samping input API Key untuk panduan mendapatkan key.'
182
+ return
183
+
184
+ genai.configure(api_key=api_key)
185
+ model = genai.GenerativeModel('gemini-pro')
186
+
187
+ # Tambahkan konteks repository jika ada
188
+ if self.current_repo:
189
+ context = f'Repository: {self.current_repo}\n\n'
190
+ repo_files = '\n'.join(list(self.repo_content.keys()))
191
+ context += f'Files in repository:\n{repo_files}\n\n'
192
+ prompt = context + prompt
193
+
194
+ response = model.generate_content(
195
+ prompt,
196
+ generation_config={'temperature': 0.7, 'top_p': 0.8, 'top_k': 40},
197
+ stream=True,
198
+ )
199
+
200
+ full_response = ''
201
+ async for chunk in response:
202
+ if chunk.text:
203
+ full_response += chunk.text
204
+ yield chunk.text
205
+
206
+ self.chat_history.append({'role': 'user', 'content': prompt})
207
+ self.chat_history.append({'role': 'assistant', 'content': full_response})
208
+
209
+ except Exception as e:
210
+ error_msg = f'⚠️ Error dalam Gemini API: {str(e)}\n\nPastikan API Key valid dan memiliki kuota yang cukup.'
211
+ print(error_msg)
212
+ yield error_msg
213
+
214
+ def clone_repository(
215
+ self, repo_url: str, github_token: str, branch: str = None
216
+ ) -> tuple[bool, str]:
217
+ """Clone repository GitHub dengan autentikasi"""
218
+ if not repo_url:
219
+ return False, '⚠️ URL repository diperlukan'
220
+
221
+ repo_name = repo_url.split('/')[-1].replace('.git', '')
222
+
223
+ if os.path.exists(repo_name):
224
+ subprocess.run(['rm', '-rf', repo_name], check=True)
225
+
226
+ try:
227
+ owner_repo = '/'.join(repo_url.split('/')[-2:])
228
+
229
+ # Cek apakah repository private
230
+ headers = {'Authorization': f'token {github_token}'} if github_token else {}
231
+ repo_check = requests.get(
232
+ f'https://api.github.com/repos/{owner_repo}', headers=headers
233
+ )
234
+
235
+ if repo_check.status_code == 404:
236
+ return False, '⚠️ Repository tidak ditemukan. Periksa URL repository.'
237
+ elif repo_check.status_code == 401:
238
+ return (
239
+ False,
240
+ '⚠️ Token GitHub tidak valid. Klik icon bantuan (?) untuk panduan mendapatkan token.',
241
+ )
242
+ elif repo_check.status_code == 403 and repo_check.json().get(
243
+ 'private', False
244
+ ):
245
+ return (
246
+ False,
247
+ "⚠️ Ini adalah repository private. Token GitHub dengan akses 'repo' diperlukan.",
248
+ )
249
+
250
+ auth_url = (
251
+ f'https://{github_token}@github.com/{owner_repo}'
252
+ if github_token
253
+ else f'https://github.com/{owner_repo}'
254
+ )
255
+
256
+ cmd = ['git', 'clone']
257
+ if branch:
258
+ cmd.extend(['--branch', branch])
259
+ cmd.append(auth_url)
260
+
261
+ process = subprocess.run(
262
+ cmd,
263
+ capture_output=True,
264
+ text=True,
265
+ env=dict(os.environ, GIT_ASKPASS='echo', GIT_TERMINAL_PROMPT='0'),
266
+ )
267
+
268
+ if process.returncode == 0:
269
+ self.current_repo = repo_name
270
+ # Scan dan simpan konten repository
271
+ file_count = 0
272
+ for file_path in Path(repo_name).rglob('*'):
273
+ if file_path.is_file() and '.git' not in str(file_path):
274
+ success, content = self.read_file_safely(str(file_path))
275
+ if success:
276
+ self.repo_content[str(file_path)] = content
277
+ file_count += 1
278
+
279
+ return (
280
+ True,
281
+ f'βœ… Repository berhasil di-clone!\n\nNama: {repo_name}\nJumlah file: {file_count}\n\nAnda sekarang bisa mengajukan pertanyaan tentang repository ini.',
282
+ )
283
+ else:
284
+ return False, f'⚠️ Gagal clone repository:\n{process.stderr}'
285
+
286
+ except Exception as e:
287
+ return False, f'⚠️ Error: {str(e)}'
288
+
289
+ def read_file_safely(self, file_path: str) -> tuple[bool, str]:
290
+ """Baca file dengan aman menggunakan berbagai encoding"""
291
+ encodings = ['utf-8', 'latin-1', 'cp1252']
292
+ for encoding in encodings:
293
+ try:
294
+ with open(file_path, 'r', encoding=encoding) as f:
295
+ content = f.read()
296
+ return True, content
297
+ except Exception as e:
298
+ continue
299
+ return False, 'Tidak dapat membaca file dengan encoding yang didukung'
300
+
301
+
302
+ analyzer = RepoAnalyzer()
303
+
304
+
305
+ async def handle_chat(
306
+ message,
307
+ history,
308
+ provider_choice,
309
+ model_name,
310
+ xai_key,
311
+ gemini_key,
312
+ selected_files,
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
+
329
+ # Format user message
330
+ user_message = f'<div class="user-message">{message}</div>'
331
+ history.append({'role': 'user', 'content': user_message})
332
+
333
+ # Initialize assistant message
334
+ assistant_message = '<div class="assistant-message">'
335
+ history.append({'role': 'assistant', 'content': assistant_message})
336
+
337
+ try:
338
+ # Add context about selected files to the prompt
339
+ file_context = ''
340
+ if selected_files:
341
+ file_context = '\n\nFile yang dipilih:\n'
342
+ for file in selected_files:
343
+ content = analyzer.repo_content.get(file, '')
344
+ if content:
345
+ file_context += f'\n{file}:\n```\n{content}\n```\n'
346
+
347
+ enhanced_message = f'{message}\n{file_context}'
348
+
349
+ full_response = ''
350
+ if provider_choice == AIProvider.XAI:
351
+ async for chunk in analyzer.stream_xai_response(
352
+ enhanced_message, xai_key, model_name
353
+ ):
354
+ full_response += chunk
355
+ # Format the response with code blocks
356
+ formatted_response = format_response(full_response)
357
+ history[-1]['content'] = (
358
+ f'<div class="assistant-message">{formatted_response}</div>'
359
+ )
360
+ await asyncio.sleep(0.05)
361
+ yield history
362
+
363
+ elif provider_choice == AIProvider.GEMINI:
364
+ async for chunk in analyzer.stream_gemini_response(
365
+ enhanced_message, gemini_key or DEFAULT_GEMINI_KEY
366
+ ):
367
+ full_response += chunk
368
+ formatted_response = format_response(full_response)
369
+ history[-1]['content'] = (
370
+ f'<div class="assistant-message">{formatted_response}</div>'
371
+ )
372
+ await asyncio.sleep(0.05)
373
+ yield history
374
+
375
+ else: # OLLAMA
376
+ response = analyze_with_ollama(model_name, enhanced_message)
377
+ words = response.split()
378
+ for i in range(len(words)):
379
+ full_response = ' '.join(words[: i + 1])
380
+ formatted_response = format_response(full_response)
381
+ history[-1]['content'] = (
382
+ f'<div class="assistant-message">{formatted_response}</div>'
383
+ )
384
+ await asyncio.sleep(0.05)
385
+ yield history
386
+
387
+ except Exception as e:
388
+ error_msg = f'⚠️ Error: {str(e)}'
389
+ history[-1]['content'] = f'<div class="assistant-message">{error_msg}</div>'
390
+ yield history
391
+
392
+
393
+ def format_response(text):
394
+ """Format response text with code blocks"""
395
+ import re
396
+ import uuid
397
+
398
+ # Replace code blocks with styled versions
399
+ def replace_code_block(match):
400
+ language = match.group(1) or ''
401
+ code = match.group(2)
402
+ block_id = f'code-block-{uuid.uuid4().hex[:8]}'
403
+
404
+ return f'''
405
+ <div class="code-block">
406
+ <div class="code-header">
407
+ <span class="code-title">{language}</span>
408
+ <button class="code-copy-btn" id="{block_id}-btn" onclick="copyCode('{block_id}')">Copy</button>
409
+ </div>
410
+ <pre class="code-content" id="{block_id}">{code}</pre>
411
+ </div>
412
+ '''
413
+
414
+ # Replace ```language\ncode``` blocks
415
+ text = re.sub(r'```(\w+)?\n(.*?)```', replace_code_block, text, flags=re.DOTALL)
416
+
417
+ # Replace inline code
418
+ text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
419
+
420
+ return text
421
+
422
+
423
+ def create_ui():
424
+ global analyzer
425
+ current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
426
+
427
+ with gr.Blocks(title='Open Repo AI', theme=gr.themes.Soft()) as app:
428
+ # CSS Styling
429
+ gr.Markdown("""
430
+ <style>
431
+ /* General Styles */
432
+ .container { max-width: 100% !important; padding: 1rem; }
433
+ .mobile-full { width: 100% !important; }
434
+
435
+ /* Header Styles */
436
+ .header-info {
437
+ display: flex;
438
+ justify-content: space-between;
439
+ padding: 10px;
440
+ background: #f8f9fa;
441
+ border-radius: 4px;
442
+ margin: 10px 0;
443
+ font-family: monospace;
444
+ }
445
+ .timestamp, .user-info {
446
+ padding: 5px 10px;
447
+ background: #fff;
448
+ border-radius: 4px;
449
+ border: 1px solid #e9ecef;
450
+ }
451
+
452
+ /* File List Styles */
453
+ .file-list {
454
+ margin: 10px 0;
455
+ padding: 10px;
456
+ border: 1px solid #ddd;
457
+ border-radius: 4px;
458
+ }
459
+ .file-item {
460
+ display: flex;
461
+ justify-content: space-between;
462
+ padding: 5px 0;
463
+ }
464
+
465
+ /* Collapsible Sections */
466
+ details {
467
+ margin: 10px 0;
468
+ border: 1px solid #e9ecef;
469
+ border-radius: 8px;
470
+ overflow: hidden;
471
+ }
472
+ summary {
473
+ padding: 15px;
474
+ background: #f8f9fa;
475
+ cursor: pointer;
476
+ font-weight: 600;
477
+ display: flex;
478
+ align-items: center;
479
+ transition: background-color 0.3s;
480
+ }
481
+ summary:hover {
482
+ background: #e9ecef;
483
+ }
484
+ .help-content {
485
+ padding: 15px;
486
+ background: white;
487
+ }
488
+
489
+ /* Example Sections */
490
+ .examples-wrapper {
491
+ margin: 20px 0;
492
+ }
493
+ .example-section {
494
+ margin: 10px 0;
495
+ }
496
+ .example-content {
497
+ padding: 15px;
498
+ background: white;
499
+ }
500
+ .example-btn {
501
+ width: 100%;
502
+ text-align: left;
503
+ padding: 8px 12px;
504
+ background: #f8f9fa;
505
+ border: 1px solid #e9ecef;
506
+ border-radius: 4px;
507
+ cursor: pointer;
508
+ transition: all 0.3s;
509
+ }
510
+ .example-btn:hover {
511
+ background: #e9ecef;
512
+ border-color: #dee2e6;
513
+ }
514
+
515
+ /* Chat Message Styling */
516
+ .message-wrap {
517
+ display: flex;
518
+ flex-direction: column;
519
+ gap: 1rem;
520
+ }
521
+ .user-message, .assistant-message {
522
+ padding: 1rem;
523
+ border-radius: 8px;
524
+ max-width: 90%;
525
+ }
526
+ .user-message {
527
+ align-self: flex-end;
528
+ background: #007AFF;
529
+ color: white;
530
+ }
531
+ .assistant-message {
532
+ align-self: flex-start;
533
+ background: #f8f9fa;
534
+ border: 1px solid #e9ecef;
535
+ }
536
+
537
+ /* Code Block Styling */
538
+ .code-block {
539
+ position: relative;
540
+ margin: 1rem 0;
541
+ border-radius: 8px;
542
+ overflow: hidden;
543
+ border: 1px solid #e9ecef;
544
+ }
545
+ .code-header {
546
+ display: flex;
547
+ justify-content: space-between;
548
+ align-items: center;
549
+ padding: 0.5rem 1rem;
550
+ background: #f8f9fa;
551
+ border-bottom: 1px solid #e9ecef;
552
+ }
553
+ .code-title {
554
+ font-family: monospace;
555
+ font-weight: bold;
556
+ color: #495057;
557
+ }
558
+ .code-copy-btn {
559
+ padding: 4px 8px;
560
+ font-size: 12px;
561
+ color: #6c757d;
562
+ background: white;
563
+ border: 1px solid #ced4da;
564
+ border-radius: 4px;
565
+ cursor: pointer;
566
+ transition: all 0.2s;
567
+ }
568
+ .code-copy-btn:hover {
569
+ background: #e9ecef;
570
+ border-color: #adb5bd;
571
+ }
572
+ .code-content {
573
+ padding: 1rem;
574
+ background: #282a36;
575
+ color: #f8f8f2;
576
+ overflow-x: auto;
577
+ font-family: monospace;
578
+ }
579
+
580
+ /* Media Queries */
581
+ @media (max-width: 768px) {
582
+ .gr-form { flex-direction: column !important; }
583
+ .gr-group { margin: 0.5rem 0 !important; }
584
+ }
585
+ </style>
586
+
587
+ <script>
588
+ function copyCode(blockId) {
589
+ const codeBlock = document.getElementById(blockId);
590
+ const code = codeBlock.innerText;
591
+ navigator.clipboard.writeText(code).then(() => {
592
+ const btn = document.querySelector(`#${blockId}-btn`);
593
+ btn.innerText = 'Copied!';
594
+ setTimeout(() => {
595
+ btn.innerText = 'Copy';
596
+ }, 2000);
597
+ });
598
+ }
599
+
600
+ function setQuestion(text) {
601
+ const chatInput = document.querySelector('textarea[data-testid="textbox"]');
602
+ if (chatInput) {
603
+ chatInput.value = text;
604
+ chatInput.focus();
605
+ }
606
+ }
607
+ </script>
608
+ """)
609
+
610
+ # Header
611
+ with gr.Row(elem_classes='container'):
612
+ gr.Markdown(f"""
613
+ # AI Github Repository Chat
614
+
615
+ <div class="header-info">
616
+ <span class="timestamp">Current Date and Time (UTC - YYYY-MM-DD HH:MM:SS formatted): {current_time}</span>
617
+ <span class="user-info">Current User's Login: {CURRENT_USER}</span>
618
+ </div>
619
+ """)
620
+
621
+ # Main Tabs
622
+ # Main Tabs Container
623
+ with gr.Tabs() as tabs:
624
+ # Configuration Tab
625
+ with gr.Tab('πŸ› οΈ Konfigurasi'):
626
+ provider = gr.Radio(
627
+ choices=[AIProvider.XAI, AIProvider.GEMINI, AIProvider.OLLAMA],
628
+ label='Penyedia AI',
629
+ value=AIProvider.XAI,
630
+ )
631
+
632
+ with gr.Group() as api_settings:
633
+ # X.AI API Key section
634
+ with gr.Row():
635
+ with gr.Column(scale=3):
636
+ xai_key = gr.Textbox(
637
+ label='X.AI (Grok) API Key',
638
+ type='password',
639
+ placeholder='Opsional - Klik (?) untuk info',
640
+ show_label=True,
641
+ )
642
+ with gr.Column(scale=1):
643
+ gr.Markdown("""
644
+ <details>
645
+ <summary>❓ Cara Mendapatkan X.AI API Key</summary>
646
+ <div class="help-content">
647
+ <ol>
648
+ <li>Kunjungi <a href="https://x.ai" target="_blank">X.AI Developer Portal</a></li>
649
+ <li>Daftar/Login ke akun Anda</li>
650
+ <li>Buat API Key baru</li>
651
+ <li>Salin API Key</li>
652
+ </ol>
653
+ <p><strong>Catatan:</strong></p>
654
+ <ul>
655
+ <li>Jika tidak diisi, akan menggunakan API key default</li>
656
+ <li>Masukkan API key Anda sendiri jika default mencapai limit</li>
657
+ </ul>
658
+ </div>
659
+ </details>
660
+ """)
661
+
662
+ # Gemini API Key section
663
+ with gr.Row():
664
+ with gr.Column(scale=3):
665
+ gemini_key = gr.Textbox(
666
+ label='Gemini API Key',
667
+ type='password',
668
+ placeholder='Opsional - Klik (?) untuk info',
669
+ show_label=True,
670
+ )
671
+ with gr.Column(scale=1):
672
+ gr.Markdown("""
673
+ <details>
674
+ <summary>❓ Cara Mendapatkan Gemini API Key</summary>
675
+ <div class="help-content">
676
+ <ol>
677
+ <li>Kunjungi <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a></li>
678
+ <li>Login dengan akun Google Anda</li>
679
+ <li>Klik "Create API Key"</li>
680
+ <li>Salin API Key yang dihasilkan</li>
681
+ </ol>
682
+ <p><strong>Catatan:</strong></p>
683
+ <ul>
684
+ <li>Gemini memberikan kuota gratis setiap bulan</li>
685
+ <li>Key bisa dibuat ulang jika diperlukan</li>
686
+ <li>Monitor penggunaan di <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a></li>
687
+ </ul>
688
+ </div>
689
+ </details>
690
+ """)
691
+
692
+ with gr.Row():
693
+ model_dropdown = gr.Dropdown(
694
+ label='Model AI',
695
+ choices=XAI_MODELS,
696
+ value='grok-2-latest',
697
+ interactive=True,
698
+ )
699
+
700
+ # Repository Analysis Tab
701
+ with gr.Tab('πŸ“Š Analisis Repository'):
702
+ with gr.Group():
703
+ # Repository URL input
704
+ with gr.Row():
705
+ repo_url = gr.Textbox(
706
+ label='URL Repository GitHub',
707
+ placeholder='https://github.com/username/repository',
708
+ elem_classes='mobile-full',
709
+ )
710
+
711
+ # GitHub Token section
712
+ with gr.Row():
713
+ with gr.Column(scale=2):
714
+ github_token = gr.Textbox(
715
+ label='Token GitHub',
716
+ type='password',
717
+ placeholder='Klik (?) untuk panduan',
718
+ elem_classes='mobile-full',
719
+ )
720
+ gr.Markdown("""
721
+ <details>
722
+ <summary>❓ Cara Mendapatkan GitHub Token</summary>
723
+ <div class="help-content">
724
+ <ol>
725
+ <li>Kunjungi <a href="https://github.com/settings/tokens" target="_blank">GitHub Token Settings</a></li>
726
+ <li>Klik "Generate new token" > "Generate new token (classic)"</li>
727
+ <li>Beri nama token Anda di "Note"</li>
728
+ <li>Pilih scope:
729
+ <ul>
730
+ <li><code>repo</code> (untuk akses repository private)</li>
731
+ <li><code>read:packages</code> (opsional, untuk akses package)</li>
732
+ </ul>
733
+ </li>
734
+ <li>Klik "Generate token"</li>
735
+ <li><strong>PENTING:</strong> Salin token segera! Token hanya ditampilkan sekali</li>
736
+ </ol>
737
+ </div>
738
+ </details>
739
+ """)
740
+ with gr.Column(scale=1):
741
+ branch = gr.Textbox(
742
+ label='Branch (opsional)',
743
+ placeholder='main',
744
+ elem_classes='mobile-full',
745
+ )
746
+
747
+ # Clone Button and Status
748
+ clone_button = gr.Button(
749
+ 'πŸ”„ Clone Repository',
750
+ variant='primary',
751
+ elem_classes='mobile-full',
752
+ )
753
+
754
+ clone_status = gr.Markdown(
755
+ value='', label='Status Repository', elem_classes='mobile-full'
756
+ )
757
+
758
+ # File Selection
759
+ with gr.Group():
760
+ gr.Markdown('### πŸ“Ž File yang Dipilih')
761
+ with gr.Row():
762
+ file_selector = gr.Dropdown(
763
+ label='Pilih File dari Repository',
764
+ choices=[],
765
+ multiselect=True,
766
+ value=[],
767
+ allow_custom_value=True,
768
+ max_choices=None,
769
+ elem_classes='mobile-full',
770
+ )
771
+
772
+ file_list = gr.HTML(
773
+ value="<div class='file-list'>Belum ada file yang dipilih</div>",
774
+ label='Daftar File Terpilih',
775
+ )
776
+
777
+ # Examples Tab
778
+ with gr.Tab('πŸ’‘ Examples'):
779
+ with gr.Group():
780
+ gr.Markdown("""
781
+ ### 🎯 Contoh Pertanyaan
782
+ <p>Klik pada kategori untuk melihat contoh pertanyaan yang bisa diajukan</p>
783
+ """)
784
+
785
+ gr.Markdown("""
786
+ <div class="examples-wrapper">
787
+ <details class="example-section">
788
+ <summary>πŸ” Analisis Kode</summary>
789
+ <div class="example-content">
790
+ <ul>
791
+ <li><button class="example-btn" onclick="setQuestion('Jelaskan logika dan alur dari kode ini')">Jelaskan logika dari kode</button></li>
792
+ <li><button class="example-btn" onclick="setQuestion('Bagaimana cara mengoptimalkan performa kode ini?')">Optimasi performa</button></li>
793
+ <li><button class="example-btn" onclick="setQuestion('Apakah ada potential bugs yang perlu diperbaiki?')">Cek potential bugs</button></li>
794
+ </ul>
795
+ </div>
796
+ </details>
797
+
798
+ <details class="example-section">
799
+ <summary>πŸ“š Best Practices</summary>
800
+ <div class="example-content">
801
+ <ul>
802
+ <li><button class="example-btn" onclick="setQuestion('Apa saja best practices yang bisa diterapkan di kode ini?')">Saran best practices</button></li>
803
+ <li><button class="example-btn" onclick="setQuestion('Bagaimana cara meningkatkan maintainability kode ini?')">Improve maintainability</button></li>
804
+ <li><button class="example-btn" onclick="setQuestion('Berikan saran untuk meningkatkan code quality')">Improve code quality</button></li>
805
+ </ul>
806
+ </div>
807
+ </details>
808
+
809
+ <details class="example-section">
810
+ <summary>πŸ§ͺ Testing</summary>
811
+ <div class="example-content">
812
+ <ul>
813
+ <li><button class="example-btn" onclick="setQuestion('Buatkan unit test untuk fungsi ini')">Generate unit tests</button></li>
814
+ <li><button class="example-btn" onclick="setQuestion('Bagaimana cara test error handling yang baik?')">Test error handling</button></li>
815
+ <li><button class="example-btn" onclick="setQuestion('Berikan saran untuk meningkatkan test coverage')">Improve test coverage</button></li>
816
+ </ul>
817
+ </div>
818
+ </details>
819
+
820
+ <details class="example-section">
821
+ <summary>πŸ“ Dokumentasi</summary>
822
+ <div class="example-content">
823
+ <ul>
824
+ <li><button class="example-btn" onclick="setQuestion('Buatkan dokumentasi untuk fungsi-fungsi utama')">Generate documentation</button></li>
825
+ <li><button class="example-btn" onclick="setQuestion('Buat README.md untuk project ini')">Create README.md</button></li>
826
+ <li><button class="example-btn" onclick="setQuestion('Jelaskan cara penggunaan library/framework')">Usage guide</button></li>
827
+ </ul>
828
+ </div>
829
+ </details>
830
+ </div>
831
+ """)
832
+
833
+ # Chat Interface (outside tabs)
834
+ with gr.Group():
835
+ chat_history = gr.Chatbot(
836
+ label='πŸ“ Riwayat Chat',
837
+ height=500,
838
+ show_label=True,
839
+ type='messages',
840
+ elem_classes='mobile-full',
841
+ )
842
+
843
+ with gr.Row():
844
+ chat_input = gr.Textbox(
845
+ label='πŸ’­ Tanyakan tentang Repository',
846
+ placeholder='Ketik pertanyaan Anda di sini...',
847
+ lines=3,
848
+ elem_classes='mobile-full',
849
+ )
850
+ send_button = gr.Button('πŸ“€ Kirim', variant='primary')
851
+ clear_button = gr.Button('🧹 Bersihkan', variant='secondary')
852
+
853
+ # Event Handlers
854
+ def handle_clone(repo_url, github_token, branch):
855
+ if not repo_url:
856
+ return (
857
+ '⚠️ URL repository diperlukan!',
858
+ gr.Dropdown(choices=[]),
859
+ "<div class='file-list'>Belum ada file yang dipilih</div>",
860
+ )
861
+
862
+ success, message = analyzer.clone_repository(repo_url, github_token, branch)
863
+
864
+ if success:
865
+ files = sorted(list(analyzer.repo_content.keys()))
866
+ return (
867
+ message,
868
+ gr.Dropdown(choices=files, value=[]),
869
+ "<div class='file-list'>Belum ada file yang dipilih</div>",
870
+ )
871
+
872
+ return (
873
+ message,
874
+ gr.Dropdown(choices=[]),
875
+ "<div class='file-list'>Belum ada file yang dipilih</div>",
876
+ )
877
+
878
+ def update_file_list(selected):
879
+ if not selected:
880
+ return "<div class='file-list'>Belum ada file yang dipilih</div>"
881
+
882
+ html = "<div class='file-list'>"
883
+ for file in selected:
884
+ html += f"<div class='file-item'><span>{file}</span></div>"
885
+ html += '</div>'
886
+ return html
887
+
888
+ def clear_chat_history():
889
+ return []
890
+
891
+ def update_model_list(provider_choice):
892
+ if provider_choice == AIProvider.XAI:
893
+ return gr.Dropdown(choices=XAI_MODELS, value='grok-2-latest')
894
+ elif provider_choice == AIProvider.GEMINI:
895
+ return gr.Dropdown(choices=GEMINI_MODELS, value='gemini-1.5-mini')
896
+ else: # OLLAMA
897
+ return gr.Dropdown(choices=OLLAMA_MODELS, value='llama2')
898
+
899
+ # Connect Events
900
+ provider.change(
901
+ fn=update_model_list, inputs=[provider], outputs=[model_dropdown]
902
+ )
903
+
904
+ clone_button.click(
905
+ fn=handle_clone,
906
+ inputs=[repo_url, github_token, branch],
907
+ outputs=[clone_status, file_selector, file_list],
908
+ )
909
+
910
+ file_selector.change(
911
+ fn=update_file_list, inputs=[file_selector], outputs=[file_list]
912
+ )
913
+
914
+ # Chat events
915
+ clear_button.click(fn=clear_chat_history, outputs=[chat_history])
916
+
917
+ send_button.click(
918
+ fn=handle_chat,
919
+ inputs=[
920
+ chat_input,
921
+ chat_history,
922
+ provider,
923
+ model_dropdown,
924
+ xai_key,
925
+ gemini_key,
926
+ file_selector,
927
+ ],
928
+ outputs=chat_history,
929
+ show_progress=True,
930
+ ).then(fn=lambda: gr.update(value=''), outputs=chat_input)
931
+
932
+ chat_input.submit(
933
+ fn=handle_chat,
934
+ inputs=[
935
+ chat_input,
936
+ chat_history,
937
+ provider,
938
+ model_dropdown,
939
+ xai_key,
940
+ gemini_key,
941
+ file_selector,
942
+ ],
943
+ outputs=chat_history,
944
+ show_progress=True,
945
+ ).then(fn=lambda: gr.update(value=''), outputs=chat_input)
946
+
947
+ return app
948
+
949
+
950
+ if __name__ == '__main__':
951
+ print("""
952
+ πŸš€ Starting Repository Chat Analysis
953
+ ====================================
954
+ """)
955
+
956
+ app = create_ui()
957
+ app.launch(share=True, server_name='0.0.0.0', server_port=7860, debug=True)