nihalaninihal commited on
Commit
0a3a291
·
verified ·
1 Parent(s): d15f6d2

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +2134 -0
  2. index-html.html +587 -0
  3. requirements.txt +29 -0
app.py ADDED
@@ -0,0 +1,2134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ import os
5
+ import pathlib
6
+ from typing import AsyncGenerator, Dict, List, Any, Tuple, Optional, Set, Literal
7
+
8
+ import gradio as gr
9
+ import numpy as np
10
+ from dotenv import load_dotenv
11
+ from fastapi import FastAPI
12
+ from fastapi.responses import HTMLResponse
13
+ from fastrtc import (
14
+ AsyncStreamHandler,
15
+ Stream,
16
+ get_twilio_turn_credentials,
17
+ wait_for_item,
18
+ )
19
+ from github import Github
20
+ from google import genai
21
+ from google.genai.types import (
22
+ LiveConnectConfig,
23
+ PrebuiltVoiceConfig,
24
+ SpeechConfig,
25
+ VoiceConfig,
26
+ )
27
+ from gradio.utils import get_space
28
+ from pydantic import BaseModel
29
+ from collections import defaultdict
30
+ import base64
31
+ from pathlib import Path
32
+ import tempfile
33
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
34
+ import re
35
+ import requests
36
+ from datetime import datetime
37
+
38
+ # Set up paths
39
+ current_dir = pathlib.Path(__file__).parent
40
+ index_html_path = current_dir / "index.html"
41
+
42
+ # Load environment variables
43
+ load_dotenv()
44
+
45
+ # Configure API keys
46
+ GITHUB_TOKEN = os.getenv("GITHUB_API_TOKEN")
47
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
48
+ TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
49
+ TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
50
+
51
+ if not GITHUB_TOKEN:
52
+ GITHUB_TOKEN = "YOUR_GITHUB_TOKEN" # Will be replaced by user input
53
+
54
+ if not GEMINI_API_KEY:
55
+ GEMINI_API_KEY = "YOUR_GEMINI_API_KEY" # Will be replaced by user input
56
+
57
+ # Initialize GitHub API
58
+ gh = None
59
+
60
+ # Configure Gemini model
61
+ def configure_gemini(api_key):
62
+ genai.configure(api_key=api_key)
63
+ return genai.GenerativeModel(
64
+ model_name="gemini-1.5-pro-latest",
65
+ generation_config={
66
+ "temperature": 0.7,
67
+ "top_p": 0.95,
68
+ "top_k": 40,
69
+ "max_output_tokens": 8192,
70
+ },
71
+ safety_settings=[
72
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
73
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
74
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
75
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
76
+ ]
77
+ )
78
+
79
+ # Configure Gemini client for voice
80
+ def create_gemini_client(api_key):
81
+ return genai.Client(
82
+ api_key=api_key,
83
+ http_options={"api_version": "v1alpha"},
84
+ )
85
+
86
+ # Audio encoding function
87
+ def encode_audio(data: np.ndarray) -> str:
88
+ """Encode Audio data to send to the server"""
89
+ return base64.b64encode(data.tobytes()).decode("UTF-8")
90
+
91
+ # Code file extensions to analyze
92
+ RELEVANT_EXTENSIONS = {
93
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".cpp", ".c", ".h",
94
+ ".hpp", ".rb", ".php", ".go", ".rs", ".swift", ".kt", ".cs", ".css",
95
+ ".html", ".xml", ".json", ".yaml", ".yml", ".md", ".sh", ".bat"
96
+ }
97
+
98
+ # Repository analysis class
99
+ class RepositoryAnalyzer:
100
+ """Handles GitHub repository analysis"""
101
+
102
+ def __init__(self, repo_url: str, github_token: str):
103
+ # Extract owner and repo name from URL
104
+ parts = repo_url.rstrip('/').split('/')
105
+ if len(parts) < 2:
106
+ raise ValueError("Invalid repository URL format")
107
+
108
+ self.repo_name = parts[-1]
109
+ self.owner = parts[-2]
110
+ self.repo_url = repo_url
111
+ self.github_token = github_token
112
+
113
+ # Initialize GitHub API
114
+ self.gh = Github(github_token)
115
+ self.repo = self.gh.get_repo(f"{self.owner}/{self.repo_name}")
116
+ self.analysis_data: Dict[str, Any] = {}
117
+
118
+ # Store repository content cache
119
+ self.file_content_cache = {}
120
+
121
+ def analyze(self, progress_callback=None) -> Dict[str, Any]:
122
+ """Perform complete repository analysis"""
123
+ try:
124
+ if progress_callback:
125
+ progress_callback(0.1, "Fetching basic repository information...")
126
+
127
+ # Basic repository information
128
+ self.analysis_data["basic_info"] = self._get_basic_info()
129
+
130
+ if progress_callback:
131
+ progress_callback(0.2, "Analyzing repository structure...")
132
+
133
+ # Analyze repository structure
134
+ self.analysis_data["structure"] = self._analyze_structure()
135
+
136
+ if progress_callback:
137
+ progress_callback(0.3, "Analyzing repository dependencies...")
138
+
139
+ # Analyze dependencies
140
+ self.analysis_data["dependencies"] = self._analyze_dependencies()
141
+
142
+ if progress_callback:
143
+ progress_callback(0.4, "Analyzing code patterns...")
144
+
145
+ # Analyze code patterns
146
+ self.analysis_data["code_patterns"] = self._analyze_code_patterns()
147
+
148
+ if progress_callback:
149
+ progress_callback(0.6, "Analyzing commit history...")
150
+
151
+ # Analyze commit history
152
+ self.analysis_data["commit_history"] = self._analyze_commits()
153
+
154
+ if progress_callback:
155
+ progress_callback(0.8, "Analyzing contributors...")
156
+
157
+ # Get contributor statistics
158
+ self.analysis_data["contributors"] = self._analyze_contributors()
159
+
160
+ if progress_callback:
161
+ progress_callback(0.9, "Analyzing pull requests and issues...")
162
+
163
+ # Analyze pull requests and issues
164
+ self.analysis_data["pull_requests"] = self._analyze_pull_requests()
165
+ self.analysis_data["issues"] = self._analyze_issues()
166
+
167
+ if progress_callback:
168
+ progress_callback(1.0, "Analysis complete!")
169
+
170
+ return self.analysis_data
171
+
172
+ except Exception as e:
173
+ raise Exception(f"Error analyzing repository: {str(e)}")
174
+
175
+ def _get_basic_info(self) -> Dict[str, Any]:
176
+ """Get basic repository information"""
177
+ return {
178
+ "name": self.repo.name,
179
+ "owner": self.repo.owner.login,
180
+ "description": self.repo.description or "No description available",
181
+ "stars": self.repo.stargazers_count,
182
+ "forks": self.repo.forks_count,
183
+ "watchers": self.repo.watchers_count,
184
+ "created_at": self.repo.created_at.isoformat(),
185
+ "last_updated": self.repo.updated_at.isoformat(),
186
+ "primary_language": self.repo.language or "Not specified",
187
+ "license": self.repo.license.name if self.repo.license else "No license specified",
188
+ "open_issues_count": self.repo.open_issues_count,
189
+ "is_archived": self.repo.archived,
190
+ "is_fork": self.repo.fork,
191
+ "homepage": self.repo.homepage,
192
+ "url": self.repo.html_url,
193
+ "size": self.repo.size,
194
+ "topics": self.repo.get_topics(),
195
+ }
196
+
197
+ def _analyze_structure(self) -> Dict[str, Any]:
198
+ """Analyze repository structure and organization"""
199
+ structure = {
200
+ "files": defaultdict(int),
201
+ "directories": set(),
202
+ "total_size": 0,
203
+ "readme": None,
204
+ "license": None,
205
+ "gitignore": None,
206
+ "workflow_files": [],
207
+ "test_directories": [],
208
+ "docs_directories": [],
209
+ }
210
+
211
+ try:
212
+ # Check for root-level special files
213
+ try:
214
+ readme_content = self.repo.get_readme()
215
+ structure["readme"] = {
216
+ "path": readme_content.path,
217
+ "size": readme_content.size,
218
+ }
219
+ except:
220
+ pass
221
+
222
+ try:
223
+ license_content = self.repo.get_license()
224
+ structure["license"] = {
225
+ "path": license_content.path,
226
+ "size": license_content.size,
227
+ "name": license_content.license.name if license_content.license else "Unknown"
228
+ }
229
+ except:
230
+ pass
231
+
232
+ # Analyze repository structure recursively
233
+ contents = self.repo.get_contents("")
234
+ while contents:
235
+ content = contents.pop(0)
236
+
237
+ # Identify special files
238
+ if content.path.lower() == ".gitignore":
239
+ structure["gitignore"] = content.path
240
+ elif content.path.startswith(".github/workflows/") and content.type == "file":
241
+ structure["workflow_files"].append(content.path)
242
+
243
+ # Track directories
244
+ if content.type == "dir":
245
+ structure["directories"].add(content.path)
246
+
247
+ # Identify special directories
248
+ path_lower = content.path.lower()
249
+ if "test" in path_lower or path_lower.endswith("tests"):
250
+ structure["test_directories"].append(content.path)
251
+ elif "doc" in path_lower or path_lower.endswith("docs"):
252
+ structure["docs_directories"].append(content.path)
253
+
254
+ # Get directory contents
255
+ try:
256
+ contents.extend(self.repo.get_contents(content.path))
257
+ except Exception as e:
258
+ print(f"Error getting contents of directory {content.path}: {str(e)}")
259
+
260
+ # Track files
261
+ else:
262
+ ext = Path(content.path).suffix.lower()
263
+ structure["files"][ext] += 1
264
+ structure["total_size"] += content.size
265
+ except Exception as e:
266
+ print(f"Error analyzing structure: {str(e)}")
267
+
268
+ return {
269
+ "file_types": dict(structure["files"]),
270
+ "directory_count": len(structure["directories"]),
271
+ "total_size": structure["total_size"],
272
+ "file_count": sum(structure["files"].values()),
273
+ "readme": structure["readme"],
274
+ "license": structure["license"],
275
+ "gitignore": structure["gitignore"],
276
+ "workflow_files": structure["workflow_files"],
277
+ "test_directories": structure["test_directories"],
278
+ "docs_directories": structure["docs_directories"],
279
+ }
280
+
281
+ def _analyze_dependencies(self) -> Dict[str, Any]:
282
+ """Analyze repository dependencies"""
283
+ dependencies = {
284
+ "package_managers": [],
285
+ "dependencies": {},
286
+ "has_lockfiles": False,
287
+ }
288
+
289
+ dependency_files = {
290
+ "requirements.txt": "pip",
291
+ "setup.py": "pip",
292
+ "pyproject.toml": "poetry/pip",
293
+ "Pipfile": "pipenv",
294
+ "package.json": "npm",
295
+ "pom.xml": "maven",
296
+ "build.gradle": "gradle",
297
+ "Gemfile": "bundler",
298
+ "Cargo.toml": "cargo",
299
+ "go.mod": "go",
300
+ "composer.json": "composer",
301
+ }
302
+
303
+ lockfiles = [
304
+ "package-lock.json", "yarn.lock", "Pipfile.lock", "poetry.lock",
305
+ "Gemfile.lock", "Cargo.lock", "composer.lock", "go.sum"
306
+ ]
307
+
308
+ try:
309
+ for file_path, package_manager in dependency_files.items():
310
+ try:
311
+ content = self.repo.get_contents(file_path)
312
+ if content:
313
+ dependencies["package_managers"].append(package_manager)
314
+
315
+ # Parse dependencies from common files
316
+ if file_path == "requirements.txt":
317
+ file_content = base64.b64decode(content.content).decode('utf-8')
318
+ deps = [line.strip().split('==')[0] for line in file_content.split('\n')
319
+ if line.strip() and not line.strip().startswith('#')]
320
+ dependencies["dependencies"]["pip"] = deps
321
+ elif file_path == "package.json":
322
+ file_content = base64.b64decode(content.content).decode('utf-8')
323
+ pkg_json = json.loads(file_content)
324
+ deps = list(pkg_json.get("dependencies", {}).keys())
325
+ dev_deps = list(pkg_json.get("devDependencies", {}).keys())
326
+ dependencies["dependencies"]["npm"] = {
327
+ "dependencies": deps,
328
+ "devDependencies": dev_deps
329
+ }
330
+ except:
331
+ pass
332
+
333
+ # Check for lock files
334
+ for lockfile in lockfiles:
335
+ try:
336
+ if self.repo.get_contents(lockfile):
337
+ dependencies["has_lockfiles"] = True
338
+ break
339
+ except:
340
+ pass
341
+
342
+ except Exception as e:
343
+ print(f"Error analyzing dependencies: {str(e)}")
344
+
345
+ return dependencies
346
+
347
+ def _analyze_code_patterns(self) -> Dict[str, Any]:
348
+ """Analyze code patterns and style"""
349
+ patterns = {
350
+ "samples": [],
351
+ "languages": defaultdict(int),
352
+ "complexity_metrics": defaultdict(list),
353
+ "documentation_ratio": 0,
354
+ "avg_code_to_comment_ratio": 0,
355
+ }
356
+
357
+ try:
358
+ files = self.repo.get_contents("")
359
+ analyzed = 0
360
+ total_comments = 0
361
+ total_code = 0
362
+
363
+ while files and analyzed < 10: # Analyze up to 10 files
364
+ file = files.pop(0)
365
+ if file.type == "dir":
366
+ files.extend(self.repo.get_contents(file.path))
367
+ elif Path(file.path).suffix.lower() in RELEVANT_EXTENSIONS:
368
+ try:
369
+ content = base64.b64decode(file.content).decode('utf-8')
370
+ lines = content.splitlines()
371
+
372
+ if not lines:
373
+ continue
374
+
375
+ # Count code and comment lines
376
+ code_lines = 0
377
+ comment_lines = 0
378
+ empty_lines = 0
379
+
380
+ ext = Path(file.path).suffix.lower()
381
+
382
+ # Simple comment detection based on file type
383
+ comment_markers = {
384
+ ".py": ["#"],
385
+ ".js": ["//", "/*"],
386
+ ".ts": ["//", "/*"],
387
+ ".jsx": ["//", "/*"],
388
+ ".tsx": ["//", "/*"],
389
+ ".java": ["//", "/*"],
390
+ ".cpp": ["//", "/*"],
391
+ ".c": ["//", "/*"],
392
+ ".h": ["//", "/*"],
393
+ ".hpp": ["//", "/*"],
394
+ ".rb": ["#"],
395
+ ".php": ["//", "/*", "#"],
396
+ ".go": ["//", "/*"],
397
+ ".rs": ["//", "/*"],
398
+ ".swift": ["//", "/*"],
399
+ ".kt": ["//", "/*"],
400
+ ".cs": ["//", "/*"],
401
+ }
402
+
403
+ if ext in comment_markers:
404
+ for line in lines:
405
+ line = line.strip()
406
+ if not line:
407
+ empty_lines += 1
408
+ elif any(line.startswith(marker) for marker in comment_markers[ext]):
409
+ comment_lines += 1
410
+ else:
411
+ code_lines += 1
412
+ else:
413
+ # Default counting for unknown file types
414
+ code_lines = len([line for line in lines if line.strip()])
415
+
416
+ total_code += code_lines
417
+ total_comments += comment_lines
418
+
419
+ # Calculate metrics
420
+ loc = len([line for line in lines if line.strip()])
421
+ avg_line_length = sum(len(line) for line in lines if line) / max(1, len([line for line in lines if line]))
422
+ comment_ratio = comment_lines / max(1, code_lines + comment_lines)
423
+
424
+ # Store file analysis
425
+ patterns["samples"].append({
426
+ "path": file.path,
427
+ "language": Path(file.path).suffix[1:],
428
+ "loc": loc,
429
+ "code_lines": code_lines,
430
+ "comment_lines": comment_lines,
431
+ "empty_lines": empty_lines,
432
+ "comment_ratio": round(comment_ratio, 2),
433
+ "avg_line_length": round(avg_line_length, 2)
434
+ })
435
+
436
+ patterns["languages"][Path(file.path).suffix[1:]] += loc
437
+ patterns["complexity_metrics"]["loc"].append(loc)
438
+ patterns["complexity_metrics"]["avg_line_length"].append(avg_line_length)
439
+ patterns["complexity_metrics"]["comment_ratio"].append(comment_ratio)
440
+
441
+ analyzed += 1
442
+
443
+ # Store file content in cache for later use
444
+ self.file_content_cache[file.path] = content
445
+
446
+ except Exception as e:
447
+ print(f"Error analyzing file {file.path}: {str(e)}")
448
+ continue
449
+
450
+ # Calculate aggregate metrics
451
+ if analyzed > 0:
452
+ patterns["documentation_ratio"] = round(sum(patterns["complexity_metrics"]["comment_ratio"]) / analyzed, 2)
453
+ patterns["avg_code_to_comment_ratio"] = round(total_code / max(1, total_comments), 2)
454
+
455
+ except Exception as e:
456
+ print(f"Error in code pattern analysis: {str(e)}")
457
+
458
+ return patterns
459
+
460
+ def _analyze_commits(self) -> Dict[str, Any]:
461
+ """Analyze commit history and patterns"""
462
+ commit_data = []
463
+ commit_times = []
464
+ commit_days = []
465
+ commit_authors = defaultdict(int)
466
+ commit_messages = []
467
+ recent_activity = []
468
+
469
+ try:
470
+ commits = list(self.repo.get_commits()[:100]) # Get last 100 commits
471
+
472
+ for commit in commits:
473
+ try:
474
+ # Get commit details
475
+ commit_info = {
476
+ "sha": commit.sha,
477
+ "author": commit.author.login if commit.author else "Unknown",
478
+ "date": commit.commit.author.date.isoformat(),
479
+ "message": commit.commit.message,
480
+ "changes": {
481
+ "additions": commit.stats.additions,
482
+ "deletions": commit.stats.deletions,
483
+ }
484
+ }
485
+
486
+ # Track commit data
487
+ commit_data.append(commit_info)
488
+ commit_times.append(commit.commit.author.date.hour)
489
+ commit_days.append(commit.commit.author.date.weekday())
490
+
491
+ # Track author statistics
492
+ author = commit.author.login if commit.author else "Unknown"
493
+ commit_authors[author] += 1
494
+
495
+ # Track commit messages
496
+ commit_messages.append(commit.commit.message)
497
+
498
+ # Track recent activity (last 10 commits)
499
+ if len(recent_activity) < 10:
500
+ recent_activity.append({
501
+ "author": author,
502
+ "date": commit.commit.author.date.isoformat(),
503
+ "message": commit.commit.message[:100] + ("..." if len(commit.commit.message) > 100 else ""),
504
+ })
505
+
506
+ except Exception as e:
507
+ print(f"Error processing commit {commit.sha}: {str(e)}")
508
+ continue
509
+
510
+ # Analyze commit patterns
511
+ commit_hours = defaultdict(int)
512
+ for hour in commit_times:
513
+ commit_hours[hour] += 1
514
+
515
+ commit_weekdays = defaultdict(int)
516
+ for day in commit_days:
517
+ commit_weekdays[day] += 1
518
+
519
+ # Analyze release patterns (by tag)
520
+ releases = []
521
+ for tag in self.repo.get_tags()[:10]: # Get last 10 tags
522
+ try:
523
+ releases.append({
524
+ "name": tag.name,
525
+ "commit": tag.commit.sha,
526
+ "date": tag.commit.commit.author.date.isoformat(),
527
+ })
528
+ except Exception as e:
529
+ print(f"Error processing tag {tag.name}: {str(e)}")
530
+ continue
531
+
532
+ total_commits = len(commit_data)
533
+ return {
534
+ "total_commits": total_commits,
535
+ "commit_hours": dict(commit_hours),
536
+ "commit_weekdays": dict(commit_weekdays),
537
+ "avg_additions": sum(c["changes"]["additions"] for c in commit_data) / total_commits if total_commits else 0,
538
+ "avg_deletions": sum(c["changes"]["deletions"] for c in commit_data) / total_commits if total_commits else 0,
539
+ "commit_frequency": defaultdict(int, dict(commit_authors)),
540
+ "recent_activity": recent_activity,
541
+ "releases": releases,
542
+ }
543
+
544
+ except Exception as e:
545
+ print(f"Error in commit analysis: {str(e)}")
546
+ return {
547
+ "total_commits": 0,
548
+ "commit_hours": {},
549
+ "commit_weekdays": {},
550
+ "avg_additions": 0,
551
+ "avg_deletions": 0,
552
+ "commit_frequency": {},
553
+ "recent_activity": [],
554
+ "releases": [],
555
+ }
556
+
557
+ def _analyze_contributors(self) -> Dict[str, Any]:
558
+ """Analyze contributor statistics"""
559
+ contributor_data = []
560
+ top_contributors = []
561
+
562
+ try:
563
+ contributors = list(self.repo.get_contributors())
564
+
565
+ # Get all contributors
566
+ for contributor in contributors:
567
+ contributor_data.append({
568
+ "login": contributor.login,
569
+ "contributions": contributor.contributions,
570
+ "type": contributor.type,
571
+ "url": contributor.html_url,
572
+ })
573
+
574
+ # Sort by contributions and get top 5
575
+ top_contributors = sorted(
576
+ contributor_data,
577
+ key=lambda x: x["contributions"],
578
+ reverse=True
579
+ )[:5]
580
+
581
+ except Exception as e:
582
+ print(f"Error analyzing contributors: {str(e)}")
583
+
584
+ return {
585
+ "total_contributors": len(contributor_data),
586
+ "contributors": contributor_data,
587
+ "top_contributors": top_contributors,
588
+ }
589
+
590
+ def _analyze_pull_requests(self) -> Dict[str, Any]:
591
+ """Analyze pull request patterns"""
592
+ pr_data = {
593
+ "open_prs": 0,
594
+ "closed_prs": 0,
595
+ "merged_prs": 0,
596
+ "recent_prs": [],
597
+ }
598
+
599
+ try:
600
+ # Count open PRs
601
+ open_prs = self.repo.get_pulls(state='open')
602
+ pr_data["open_prs"] = open_prs.totalCount
603
+
604
+ # Count closed PRs
605
+ closed_prs = self.repo.get_pulls(state='closed')
606
+ pr_data["closed_prs"] = closed_prs.totalCount
607
+
608
+ # Get recent PRs (last 5)
609
+ recent_prs = list(self.repo.get_pulls(state='all')[:5])
610
+ for pr in recent_prs:
611
+ pr_data["recent_prs"].append({
612
+ "number": pr.number,
613
+ "title": pr.title,
614
+ "state": pr.state,
615
+ "created_at": pr.created_at.isoformat(),
616
+ "author": pr.user.login if pr.user else "Unknown",
617
+ "is_merged": pr.merged,
618
+ "url": pr.html_url,
619
+ })
620
+
621
+ # Count merged PRs from the sample
622
+ if pr.merged:
623
+ pr_data["merged_prs"] += 1
624
+
625
+ except Exception as e:
626
+ print(f"Error analyzing pull requests: {str(e)}")
627
+
628
+ return pr_data
629
+
630
+ def _analyze_issues(self) -> Dict[str, Any]:
631
+ """Analyze issue patterns"""
632
+ issue_data = {
633
+ "open_issues": 0,
634
+ "closed_issues": 0,
635
+ "recent_issues": [],
636
+ }
637
+
638
+ try:
639
+ # Count open issues
640
+ open_issues = self.repo.get_issues(state='open')
641
+ issue_data["open_issues"] = open_issues.totalCount
642
+
643
+ # Count closed issues
644
+ closed_issues = self.repo.get_issues(state='closed')
645
+ issue_data["closed_issues"] = closed_issues.totalCount
646
+
647
+ # Get recent issues (last 5)
648
+ recent_issues = list(self.repo.get_issues(state='all')[:5])
649
+ for issue in recent_issues:
650
+ # Skip pull requests (which are also returned as issues)
651
+ if issue.pull_request is not None:
652
+ continue
653
+
654
+ issue_data["recent_issues"].append({
655
+ "number": issue.number,
656
+ "title": issue.title,
657
+ "state": issue.state,
658
+ "created_at": issue.created_at.isoformat(),
659
+ "author": issue.user.login if issue.user else "Unknown",
660
+ "labels": [label.name for label in issue.labels],
661
+ "url": issue.html_url,
662
+ })
663
+
664
+ except Exception as e:
665
+ print(f"Error analyzing issues: {str(e)}")
666
+
667
+ return issue_data
668
+
669
+ def get_file_content(self, file_path: str) -> str:
670
+ """Get content of a specific file, using cache if available"""
671
+ if file_path in self.file_content_cache:
672
+ return self.file_content_cache[file_path]
673
+
674
+ try:
675
+ content = self.repo.get_contents(file_path)
676
+ file_content = base64.b64decode(content.content).decode('utf-8')
677
+ self.file_content_cache[file_path] = file_content
678
+ return file_content
679
+ except Exception as e:
680
+ print(f"Error getting file content for {file_path}: {str(e)}")
681
+ return f"Error: Could not retrieve file content: {str(e)}"
682
+
683
+ def search_code(self, query: str) -> List[Dict[str, Any]]:
684
+ """Search for code in the repository"""
685
+ results = []
686
+
687
+ try:
688
+ # Use GitHub search API
689
+ code_results = self.gh.search_code(f"repo:{self.owner}/{self.repo_name} {query}")
690
+
691
+ for item in code_results[:10]: # Limit to 10 results
692
+ try:
693
+ file_content = self.get_file_content(item.path)
694
+
695
+ # Find matching lines
696
+ lines = file_content.splitlines()
697
+ matching_lines = []
698
+
699
+ for i, line in enumerate(lines):
700
+ if query.lower() in line.lower():
701
+ start_line = max(0, i - 2)
702
+ end_line = min(len(lines), i + 3)
703
+ context = "\n".join(lines[start_line:end_line])
704
+ matching_lines.append({
705
+ "line_number": i + 1,
706
+ "line": line,
707
+ "context": context
708
+ })
709
+
710
+ results.append({
711
+ "path": item.path,
712
+ "url": item.html_url,
713
+ "matching_lines": matching_lines[:3], # Limit to 3 matches per file
714
+ })
715
+ except Exception as e:
716
+ print(f"Error processing search result {item.path}: {str(e)}")
717
+ continue
718
+
719
+ except Exception as e:
720
+ print(f"Error searching code: {str(e)}")
721
+
722
+ return results
723
+
724
+ def get_file_list(self, pattern: Optional[str] = None) -> List[str]:
725
+ """Get list of files in the repository, optionally filtered by pattern"""
726
+ files = []
727
+
728
+ try:
729
+ queue = [("", "")] # (path, directory)
730
+
731
+ while queue:
732
+ base_path, dir_path = queue.pop(0)
733
+ full_path = f"{base_path}/{dir_path}".strip("/")
734
+
735
+ try:
736
+ contents = self.repo.get_contents(full_path or "")
737
+
738
+ for content in contents:
739
+ if content.type == "dir":
740
+ new_base = full_path
741
+ queue.append((new_base, content.name))
742
+ else:
743
+ file_path = f"{full_path}/{content.name}" if full_path else content.name
744
+
745
+ # Filter by pattern if provided
746
+ if not pattern or re.search(pattern, file_path, re.IGNORECASE):
747
+ files.append(file_path)
748
+ except Exception as e:
749
+ print(f"Error listing files in {full_path}: {str(e)}")
750
+ continue
751
+
752
+ except Exception as e:
753
+ print(f"Error getting file list: {str(e)}")
754
+
755
+ return files
756
+
757
+
758
+ # Gemini Voice Handler
759
+ class GeminiHandler(AsyncStreamHandler):
760
+ """Handler for the Gemini API voice chat"""
761
+
762
+ def __init__(
763
+ self,
764
+ expected_layout: Literal["mono"] = "mono",
765
+ output_sample_rate: int = 24000,
766
+ output_frame_size: int = 480,
767
+ analysis_data: Optional[Dict[str, Any]] = None,
768
+ system_prompt: Optional[str] = None,
769
+ ) -> None:
770
+ super().__init__(
771
+ expected_layout,
772
+ output_sample_rate,
773
+ output_frame_size,
774
+ input_sample_rate=16000,
775
+ )
776
+ self.input_queue: asyncio.Queue = asyncio.Queue()
777
+ self.output_queue: asyncio.Queue = asyncio.Queue()
778
+ self.quit: asyncio.Event = asyncio.Event()
779
+ self.analysis_data = analysis_data or {}
780
+ self.system_prompt = system_prompt or ""
781
+
782
+ def copy(self) -> "GeminiHandler":
783
+ return GeminiHandler(
784
+ expected_layout="mono",
785
+ output_sample_rate=self.output_sample_rate,
786
+ output_frame_size=self.output_frame_size,
787
+ analysis_data=self.analysis_data,
788
+ system_prompt=self.system_prompt,
789
+ )
790
+
791
+ def set_context(self, analysis_data: Dict[str, Any], system_prompt: str):
792
+ """Set the repository analysis context for voice chat"""
793
+ self.analysis_data = analysis_data
794
+ self.system_prompt = system_prompt
795
+
796
+ async def start_up(self):
797
+ if not self.phone_mode:
798
+ await self.wait_for_args()
799
+ api_key, voice_name = self.latest_args[1:]
800
+ else:
801
+ api_key, voice_name = None, "Puck"
802
+
803
+ client = genai.Client(
804
+ api_key=api_key or os.getenv("GEMINI_API_KEY"),
805
+ http_options={"api_version": "v1alpha"},
806
+ )
807
+
808
+ # Add context prefix if available
809
+ context_prefix = ""
810
+ if self.analysis_data and self.system_prompt:
811
+ context_prefix = f"""
812
+ {self.system_prompt}
813
+
814
+ Repository Analysis Data:
815
+ {json.dumps(self.analysis_data, indent=2)}
816
+
817
+ Answer questions about this repository analysis. You are now in voice-based conversation mode.
818
+ """
819
+
820
+ config = LiveConnectConfig(
821
+ response_modalities=["AUDIO"], # type: ignore
822
+ speech_config=SpeechConfig(
823
+ voice_config=VoiceConfig(
824
+ prebuilt_voice_config=PrebuiltVoiceConfig(
825
+ voice_name=voice_name,
826
+ )
827
+ )
828
+ ),
829
+ prefix=context_prefix,
830
+ )
831
+
832
+ try:
833
+ async with client.aio.live.connect(
834
+ model="gemini-2.0-flash-exp", config=config
835
+ ) as session:
836
+ async for audio in session.start_stream(
837
+ stream=self.stream(), mime_type="audio/pcm"
838
+ ):
839
+ if audio.data:
840
+ array = np.frombuffer(audio.data, dtype=np.int16)
841
+ self.output_queue.put_nowait((self.output_sample_rate, array))
842
+ except Exception as e:
843
+ print(f"Error in Gemini streaming session: {str(e)}")
844
+
845
+ async def stream(self) -> AsyncGenerator[bytes, None]:
846
+ while not self.quit.is_set():
847
+ try:
848
+ audio = await asyncio.wait_for(self.input_queue.get(), 0.1)
849
+ yield audio
850
+ except (asyncio.TimeoutError, TimeoutError):
851
+ pass
852
+
853
+ async def receive(self, frame: tuple[int, np.ndarray]) -> None:
854
+ _, array = frame
855
+ array = array.squeeze()
856
+ audio_message = encode_audio(array)
857
+ self.input_queue.put_nowait(audio_message)
858
+
859
+ async def emit(self) -> tuple[int, np.ndarray] | None:
860
+ return await wait_for_item(self.output_queue)
861
+
862
+ def shutdown(self) -> None:
863
+ self.quit.set()
864
+
865
+
866
+ # Function to analyze repository and generate summary
867
+ @retry(
868
+ retry=retry_if_exception_type(Exception),
869
+ stop=stop_after_attempt(3),
870
+ wait=wait_exponential(multiplier=1, min=4, max=10)
871
+ )
872
+ def analyze_repository(repo_url: str, github_token: str, gemini_api_key: str, progress=None) -> Tuple[str, str, Any]:
873
+ """Analyze repository and generate LLM summary with rate limit handling"""
874
+ try:
875
+ # Configure Gemini
876
+ model = configure_gemini(gemini_api_key)
877
+
878
+ # Initialize analyzer
879
+ if progress:
880
+ progress(0, desc="Initializing repository analysis...")
881
+ analyzer = RepositoryAnalyzer(repo_url, github_token)
882
+
883
+ # Perform analysis
884
+ analysis_data = analyzer.analyze(progress)
885
+
886
+ # Generate LLM summary
887
+ if progress:
888
+ progress(0.95, desc="Generating analysis summary...")
889
+
890
+ system_prompt = """You are an expert code analyst with deep experience in software architecture, development practices, and team dynamics. Analyze the provided repository data and create a detailed, insightful analysis using the following markdown template:
891
+
892
+ # Repository Analysis
893
+
894
+ ## 📊 Project Overview
895
+ [Provide a comprehensive overview including:
896
+ - Project purpose and scope
897
+ - Age and maturity of the project
898
+ - Current activity level and maintenance status
899
+ - Key metrics (stars, forks, etc.)
900
+ - Primary technologies and languages used]
901
+
902
+ ## 🏗️ Architecture and Code Organization
903
+ [Analyze in detail:
904
+ - Repository structure and organization
905
+ - Code distribution across different technologies
906
+ - File and directory organization patterns
907
+ - Project size and complexity metrics
908
+ - Code modularity and component structure
909
+ - Presence of key architectural patterns]
910
+
911
+ ## 💻 Development Practices & Code Quality
912
+ [Evaluate:
913
+ - Coding standards and consistency
914
+ - Code complexity and maintainability metrics
915
+ - Documentation practices
916
+ - Testing approach and coverage (if visible)
917
+ - Error handling and logging practices
918
+ - Use of design patterns and best practices]
919
+
920
+ ## 📈 Development Workflow & History
921
+ [Analyze:
922
+ - Commit patterns and frequency
923
+ - Release cycles and versioning
924
+ - Branch management strategy
925
+ - Code review practices
926
+ - Continuous integration/deployment indicators
927
+ - Peak development periods and cycles]
928
+
929
+ ## 👥 Team Dynamics & Collaboration
930
+ [Examine:
931
+ - Team size and composition
932
+ - Contribution patterns
933
+ - Core maintainer identification
934
+ - Community engagement level
935
+ - Communication patterns
936
+ - Collaboration efficiency]
937
+
938
+ ## 🔧 Technical Depth & Innovation
939
+ [Assess:
940
+ - Technical sophistication level
941
+ - Innovative approaches or solutions
942
+ - Complex problem-solving examples
943
+ - Performance optimization efforts
944
+ - Security considerations
945
+ - Scalability approach]
946
+
947
+ ## 🚀 Project Health & Sustainability
948
+ [Evaluate:
949
+ - Project momentum and growth trends
950
+ - Maintenance patterns
951
+ - Community health indicators
952
+ - Documentation completeness
953
+ - Onboarding friendliness
954
+ - Long-term viability indicators]
955
+
956
+ ## 💡 Key Insights & Recommendations
957
+ [Provide:
958
+ - 3-5 key strengths identified
959
+ - 3-5 potential improvement areas
960
+ - Notable patterns or practices
961
+ - Unique characteristics
962
+ - Strategic recommendations]
963
+
964
+ Please provide detailed analysis for each section while maintaining the formatting and emojis. Support insights with specific metrics and examples from the repository data where possible."""
965
+
966
+ chat = model.start_chat(history=[])
967
+ response = chat.send_message(f"{system_prompt}\n\nRepository Analysis Data:\n{json.dumps(analysis_data, indent=2)}")
968
+
969
+ # Save analysis data
970
+ if progress:
971
+ progress(0.98, desc="Saving analysis results...")
972
+
973
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
974
+ json.dump(analysis_data, f, indent=2)
975
+ analysis_file = f.name
976
+
977
+ return response.text, analysis_file, analyzer, system_prompt
978
+
979
+ except Exception as e:
980
+ error_message = f"Error analyzing repository: {str(e)}"
981
+ raise Exception(error_message)
982
+
983
+
984
+ # Function to create a chat session and ask questions
985
+ def create_chat_session(gemini_api_key: str) -> Any:
986
+ """Create a new chat session for follow-up questions"""
987
+ genai.configure(api_key=gemini_api_key)
988
+ return genai.GenerativeModel(
989
+ model_name="gemini-1.5-pro-latest",
990
+ generation_config={
991
+ 'temperature': 0.7,
992
+ 'top_p': 0.8,
993
+ 'top_k': 40,
994
+ 'max_output_tokens': 4096,
995
+ }
996
+ )
997
+
998
+
999
+ @retry(
1000
+ retry=retry_if_exception_type(Exception),
1001
+ stop=stop_after_attempt(3),
1002
+ wait=wait_exponential(multiplier=1, min=4, max=10)
1003
+ )
1004
+ def ask_question(question: str, analysis_file: str, analyzer: RepositoryAnalyzer, gemini_api_key: str,
1005
+ chat_history: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
1006
+ """Process a follow-up question about the analysis"""
1007
+ if not analysis_file or not analyzer:
1008
+ return chat_history + [(question, "Please analyze a repository first before asking questions.")]
1009
+
1010
+ try:
1011
+ # Load analysis data
1012
+ with open(analysis_file, 'r') as f:
1013
+ analysis_data = json.load(f)
1014
+
1015
+ # Initialize chat model
1016
+ model = create_chat_session(gemini_api_key)
1017
+
1018
+ # Check if this is a file content request
1019
+ file_request_match = re.search(r"(show|view|get|display|content of|code for)\s+(?:the\s+)?(?:file\s+)?['\"]?([^'\"]+?)['\"]?(?:\s+file)?",
1020
+ question.lower())
1021
+
1022
+ if file_request_match:
1023
+ file_path = file_request_match.group(2).strip()
1024
+
1025
+ # Try to find the exact file
1026
+ all_files = analyzer.get_file_list()
1027
+
1028
+ # Check for exact match
1029
+ if file_path in all_files:
1030
+ file_content = analyzer.get_file_content(file_path)
1031
+ return chat_history + [(question, f"Here's the content of `{file_path}`:\n\n```\n{file_content}\n```")]
1032
+
1033
+ # Check for partial match
1034
+ matching_files = [f for f in all_files if file_path.lower() in f.lower()]
1035
+ if matching_files:
1036
+ if len(matching_files) == 1:
1037
+ file_content = analyzer.get_file_content(matching_files[0])
1038
+ return chat_history + [(question, f"Here's the content of `{matching_files[0]}`:\n\n```\n{file_content}\n```")]
1039
+ else:
1040
+ file_list = "\n".join([f"- {f}" for f in matching_files[:10]])
1041
+ return chat_history + [(question, f"I found multiple files matching '{file_path}'. Please specify which one you'd like to see:\n\n{file_list}{' and more...' if len(matching_files) > 10 else ''}")]
1042
+
1043
+ # Check if this is a code search request
1044
+ search_request_match = re.search(r"(search|find|look for|where is)\s+(?:for\s+)?['\"]?([^'\"]+?)['\"]?(?:\s+in the code)?",
1045
+ question.lower())
1046
+
1047
+ if search_request_match:
1048
+ search_query = search_request_match.group(2).strip()
1049
+ search_results = analyzer.search_code(search_query)
1050
+
1051
+ if search_results:
1052
+ result_text = f"I found {len(search_results)} files containing '{search_query}':\n\n"
1053
+
1054
+ for result in search_results:
1055
+ result_text += f"**File: {result['path']}**\n"
1056
+
1057
+ if result['matching_lines']:
1058
+ for match in result['matching_lines']:
1059
+ result_text += f"Line {match['line_number']}: `{match['line'].strip()}`\n"
1060
+ result_text += "\n"
1061
+ else:
1062
+ result_text += "No specific line matches found.\n\n"
1063
+
1064
+ return chat_history + [(question, result_text)]
1065
+ else:
1066
+ return chat_history + [(question, f"I couldn't find any code matching '{search_query}' in the repository.")]
1067
+
1068
+ # For general questions, use the AI
1069
+ # Build context from chat history and current question
1070
+ context = "You are an expert code analyst helping users understand repository analysis results.\n\n"
1071
+ context += f"Repository Analysis Data:\n{json.dumps(analysis_data, indent=2)}\n\n"
1072
+
1073
+ # Add chat history context
1074
+ if chat_history:
1075
+ context += "Previous conversation:\n"
1076
+ for user_msg, assistant_msg in chat_history[-5:]: # Include last 5 messages only
1077
+ context += f"User: {user_msg}\nAssistant: {assistant_msg}\n"
1078
+
1079
+ # Add current question
1080
+ prompt = context + f"\nUser: {question}\nPlease provide your analysis based on the repository data:"
1081
+
1082
+ # Get response
1083
+ response = model.generate_content(prompt)
1084
+
1085
+ # Return in the correct tuple format for Gradio chatbot
1086
+ return chat_history + [(question, response.text)]
1087
+
1088
+ except Exception as e:
1089
+ error_message = f"Error processing question: {str(e)}"
1090
+ return chat_history + [(question, error_message)]
1091
+
1092
+
1093
+ # Input data models
1094
+ class InputData(BaseModel):
1095
+ webrtc_id: str
1096
+ voice_name: str
1097
+ api_key: str
1098
+ repo_url: Optional[str] = None
1099
+ github_token: Optional[str] = None
1100
+
1101
+
1102
+ # Create FastAPI app and set up routes
1103
+ app = FastAPI()
1104
+
1105
+ # Create Gemini handler for voice chat
1106
+ gemini_handler = GeminiHandler()
1107
+
1108
+ # Create voice chat stream
1109
+ voice_stream = Stream(
1110
+ modality="audio",
1111
+ mode="send-receive",
1112
+ handler=gemini_handler,
1113
+ rtc_configuration=get_twilio_turn_credentials() if get_space() else None,
1114
+ concurrency_limit=5 if get_space() else None,
1115
+ time_limit=120 if get_space() else None,
1116
+ additional_inputs=[
1117
+ gr.Textbox(
1118
+ label="Gemini API Key",
1119
+ type="password",
1120
+ value=os.getenv("GEMINI_API_KEY") if not get_space() else "",
1121
+ ),
1122
+ gr.Dropdown(
1123
+ label="Voice",
1124
+ choices=[
1125
+ "Puck",
1126
+ "Charon",
1127
+ "Kore",
1128
+ "Fenrir",
1129
+ "Aoede",
1130
+ ],
1131
+ value="Puck",
1132
+ ),
1133
+ ],
1134
+ )
1135
+
1136
+ # Mount voice stream to app
1137
+ voice_stream.mount(app)
1138
+
1139
+
1140
+ # Current repository analysis data
1141
+ current_analysis = {
1142
+ "data": None,
1143
+ "analyzer": None,
1144
+ "file": None,
1145
+ "summary": None,
1146
+ "system_prompt": None,
1147
+ }
1148
+
1149
+
1150
+ @app.post("/input_hook")
1151
+ async def _(body: InputData):
1152
+ voice_stream.set_input(body.webrtc_id, body.api_key, body.voice_name)
1153
+ # If repo data is provided, analyze it and update the context
1154
+ if body.repo_url and body.github_token and current_analysis["data"] is None:
1155
+ try:
1156
+ # Analyze the repository in a background task to not block the voice connection
1157
+ asyncio.create_task(analyze_and_update_context(body.repo_url, body.github_token, body.api_key))
1158
+ except Exception as e:
1159
+ print(f"Error analyzing repository: {str(e)}")
1160
+
1161
+ # Update handler context if analysis data exists
1162
+ if current_analysis["data"] and current_analysis["system_prompt"]:
1163
+ gemini_handler.set_context(current_analysis["data"], current_analysis["system_prompt"])
1164
+
1165
+ return {"status": "ok"}
1166
+
1167
+
1168
+ @app.post("/analyze_repository")
1169
+ async def analyze_repo(repo_url: str, github_token: str, gemini_api_key: str):
1170
+ try:
1171
+ summary, file_path, analyzer, system_prompt = await asyncio.to_thread(
1172
+ analyze_repository, repo_url, github_token, gemini_api_key
1173
+ )
1174
+
1175
+ # Load analysis data from file
1176
+ with open(file_path, 'r') as f:
1177
+ analysis_data = json.load(f)
1178
+
1179
+ # Update current analysis
1180
+ current_analysis["data"] = analysis_data
1181
+ current_analysis["analyzer"] = analyzer
1182
+ current_analysis["file"] = file_path
1183
+ current_analysis["summary"] = summary
1184
+ current_analysis["system_prompt"] = system_prompt
1185
+
1186
+ # Update handler context
1187
+ gemini_handler.set_context(analysis_data, system_prompt)
1188
+
1189
+ return {
1190
+ "status": "success",
1191
+ "summary": summary,
1192
+ "file_path": file_path
1193
+ }
1194
+ except Exception as e:
1195
+ return {
1196
+ "status": "error",
1197
+ "message": str(e)
1198
+ }
1199
+
1200
+
1201
+ async def analyze_and_update_context(repo_url: str, github_token: str, gemini_api_key: str):
1202
+ try:
1203
+ summary, file_path, analyzer, system_prompt = await asyncio.to_thread(
1204
+ analyze_repository, repo_url, github_token, gemini_api_key
1205
+ )
1206
+
1207
+ # Load analysis data from file
1208
+ with open(file_path, 'r') as f:
1209
+ analysis_data = json.load(f)
1210
+
1211
+ # Update current analysis
1212
+ current_analysis["data"] = analysis_data
1213
+ current_analysis["analyzer"] = analyzer
1214
+ current_analysis["file"] = file_path
1215
+ current_analysis["summary"] = summary
1216
+ current_analysis["system_prompt"] = system_prompt
1217
+
1218
+ # Update handler context
1219
+ gemini_handler.set_context(analysis_data, system_prompt)
1220
+ except Exception as e:
1221
+ print(f"Error analyzing repository in background: {str(e)}")
1222
+
1223
+
1224
+ @app.post("/ask_question")
1225
+ async def ask_repo_question(question: str):
1226
+ if not current_analysis["file"] or not current_analysis["analyzer"]:
1227
+ return {
1228
+ "status": "error",
1229
+ "message": "Please analyze a repository first before asking questions."
1230
+ }
1231
+
1232
+ try:
1233
+ response = await asyncio.to_thread(
1234
+ ask_question,
1235
+ question,
1236
+ current_analysis["file"],
1237
+ current_analysis["analyzer"],
1238
+ GEMINI_API_KEY,
1239
+ []
1240
+ )
1241
+
1242
+ # Extract just the response text
1243
+ _, answer = response[0]
1244
+
1245
+ return {
1246
+ "status": "success",
1247
+ "answer": answer
1248
+ }
1249
+ except Exception as e:
1250
+ return {
1251
+ "status": "error",
1252
+ "message": str(e)
1253
+ }
1254
+
1255
+
1256
+ @app.get("/")
1257
+ async def index():
1258
+ rtc_config = get_twilio_turn_credentials() if get_space() else None
1259
+
1260
+ # Check if index.html exists
1261
+ if not index_html_path.exists():
1262
+ # Create basic HTML if not exists
1263
+ html_content = """<!DOCTYPE html>
1264
+ <html lang="en">
1265
+ <head>
1266
+ <meta charset="UTF-8">
1267
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1268
+ <title>GitHub Repository Analyzer with Voice Chat</title>
1269
+ <style>
1270
+ :root {
1271
+ --color-accent: #6366f1;
1272
+ --color-background: #0f172a;
1273
+ --color-surface: #1e293b;
1274
+ --color-text: #e2e8f0;
1275
+ --boxSize: 8px;
1276
+ --gutter: 4px;
1277
+ }
1278
+
1279
+ body {
1280
+ margin: 0;
1281
+ padding: 0;
1282
+ background-color: var(--color-background);
1283
+ color: var(--color-text);
1284
+ font-family: system-ui, -apple-system, sans-serif;
1285
+ min-height: 100vh;
1286
+ display: flex;
1287
+ flex-direction: column;
1288
+ align-items: center;
1289
+ justify-content: center;
1290
+ }
1291
+
1292
+ .container {
1293
+ width: 90%;
1294
+ max-width: 800px;
1295
+ background-color: var(--color-surface);
1296
+ padding: 2rem;
1297
+ border-radius: 1rem;
1298
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1299
+ }
1300
+
1301
+ .wave-container {
1302
+ position: relative;
1303
+ display: flex;
1304
+ min-height: 100px;
1305
+ max-height: 128px;
1306
+ justify-content: center;
1307
+ align-items: center;
1308
+ margin: 2rem 0;
1309
+ }
1310
+
1311
+ .box-container {
1312
+ display: flex;
1313
+ justify-content: space-between;
1314
+ height: 64px;
1315
+ width: 100%;
1316
+ }
1317
+
1318
+ .box {
1319
+ height: 100%;
1320
+ width: var(--boxSize);
1321
+ background: var(--color-accent);
1322
+ border-radius: 8px;
1323
+ transition: transform 0.05s ease;
1324
+ }
1325
+
1326
+ .controls {
1327
+ display: grid;
1328
+ gap: 1rem;
1329
+ margin-bottom: 2rem;
1330
+ }
1331
+
1332
+ .input-group {
1333
+ display: flex;
1334
+ flex-direction: column;
1335
+ gap: 0.5rem;
1336
+ }
1337
+
1338
+ label {
1339
+ font-size: 0.875rem;
1340
+ font-weight: 500;
1341
+ }
1342
+
1343
+ input,
1344
+ select {
1345
+ padding: 0.75rem;
1346
+ border-radius: 0.5rem;
1347
+ border: 1px solid rgba(255, 255, 255, 0.1);
1348
+ background-color: var(--color-background);
1349
+ color: var(--color-text);
1350
+ font-size: 1rem;
1351
+ }
1352
+
1353
+ button {
1354
+ padding: 1rem 2rem;
1355
+ border-radius: 0.5rem;
1356
+ border: none;
1357
+ background-color: var(--color-accent);
1358
+ color: white;
1359
+ font-weight: 600;
1360
+ cursor: pointer;
1361
+ transition: all 0.2s ease;
1362
+ }
1363
+
1364
+ button:hover {
1365
+ opacity: 0.9;
1366
+ transform: translateY(-1px);
1367
+ }
1368
+
1369
+ .icon-with-spinner {
1370
+ display: flex;
1371
+ align-items: center;
1372
+ justify-content: center;
1373
+ gap: 12px;
1374
+ min-width: 180px;
1375
+ }
1376
+
1377
+ .spinner {
1378
+ width: 20px;
1379
+ height: 20px;
1380
+ border: 2px solid white;
1381
+ border-top-color: transparent;
1382
+ border-radius: 50%;
1383
+ animation: spin 1s linear infinite;
1384
+ flex-shrink: 0;
1385
+ }
1386
+
1387
+ @keyframes spin {
1388
+ to {
1389
+ transform: rotate(360deg);
1390
+ }
1391
+ }
1392
+
1393
+ .pulse-container {
1394
+ display: flex;
1395
+ align-items: center;
1396
+ justify-content: center;
1397
+ gap: 12px;
1398
+ min-width: 180px;
1399
+ }
1400
+
1401
+ .pulse-circle {
1402
+ width: 20px;
1403
+ height: 20px;
1404
+ border-radius: 50%;
1405
+ background-color: white;
1406
+ opacity: 0.2;
1407
+ flex-shrink: 0;
1408
+ transform: translateX(-0%) scale(var(--audio-level, 1));
1409
+ transition: transform 0.1s ease;
1410
+ }
1411
+
1412
+ /* Add styles for toast notifications */
1413
+ .toast {
1414
+ position: fixed;
1415
+ top: 20px;
1416
+ left: 50%;
1417
+ transform: translateX(-50%);
1418
+ padding: 16px 24px;
1419
+ border-radius: 4px;
1420
+ font-size: 14px;
1421
+ z-index: 1000;
1422
+ display: none;
1423
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1424
+ }
1425
+
1426
+ .toast.error {
1427
+ background-color: #f44336;
1428
+ color: white;
1429
+ }
1430
+
1431
+ .toast.warning {
1432
+ background-color: #ffd700;
1433
+ color: black;
1434
+ }
1435
+
1436
+ .tabs {
1437
+ display: flex;
1438
+ margin-bottom: 1rem;
1439
+ }
1440
+
1441
+ .tab {
1442
+ padding: 0.5rem 1rem;
1443
+ cursor: pointer;
1444
+ border-bottom: 2px solid transparent;
1445
+ transition: all 0.2s ease;
1446
+ }
1447
+
1448
+ .tab.active {
1449
+ border-bottom: 2px solid var(--color-accent);
1450
+ color: var(--color-accent);
1451
+ }
1452
+
1453
+ .tab-content {
1454
+ display: none;
1455
+ }
1456
+
1457
+ .tab-content.active {
1458
+ display: block;
1459
+ }
1460
+
1461
+ .chat-container {
1462
+ height: 300px;
1463
+ overflow-y: auto;
1464
+ border: 1px solid rgba(255, 255, 255, 0.1);
1465
+ border-radius: 0.5rem;
1466
+ padding: 1rem;
1467
+ margin-bottom: 1rem;
1468
+ background-color: rgba(0, 0, 0, 0.2);
1469
+ }
1470
+
1471
+ .chat-message {
1472
+ margin-bottom: 1rem;
1473
+ padding-bottom: 1rem;
1474
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1475
+ }
1476
+
1477
+ .chat-message:last-child {
1478
+ border-bottom: none;
1479
+ }
1480
+
1481
+ .chat-message .user {
1482
+ font-weight: bold;
1483
+ color: var(--color-accent);
1484
+ margin-bottom: 0.5rem;
1485
+ }
1486
+
1487
+ .chat-message .bot {
1488
+ font-weight: bold;
1489
+ color: #10b981;
1490
+ margin-bottom: 0.5rem;
1491
+ }
1492
+
1493
+ .chat-input {
1494
+ display: flex;
1495
+ gap: 0.5rem;
1496
+ }
1497
+
1498
+ .chat-input input {
1499
+ flex: 1;
1500
+ }
1501
+ </style>
1502
+ </head>
1503
+
1504
+ <body>
1505
+ <!-- Add toast element after body opening tag -->
1506
+ <div id="error-toast" class="toast"></div>
1507
+ <div style="text-align: center">
1508
+ <h1>GitHub Repository Analyzer with Voice Chat</h1>
1509
+ <p>Analyze GitHub repositories and chat with the AI using voice or text</p>
1510
+ </div>
1511
+
1512
+ <div class="container">
1513
+ <div class="tabs">
1514
+ <div class="tab active" data-tab="repo">Repository Setup</div>
1515
+ <div class="tab" data-tab="text">Text Chat</div>
1516
+ <div class="tab" data-tab="voice">Voice Chat</div>
1517
+ </div>
1518
+
1519
+ <div class="tab-content active" id="repo-tab">
1520
+ <div class="controls">
1521
+ <div class="input-group">
1522
+ <label for="github-token">GitHub API Token</label>
1523
+ <input type="password" id="github-token" placeholder="Enter your GitHub API token">
1524
+ </div>
1525
+ <div class="input-group">
1526
+ <label for="gemini-api-key">Gemini API Key</label>
1527
+ <input type="password" id="gemini-api-key" placeholder="Enter your Gemini API key">
1528
+ </div>
1529
+ <div class="input-group">
1530
+ <label for="repo-url">GitHub Repository URL</label>
1531
+ <input type="text" id="repo-url" placeholder="https://github.com/owner/repo">
1532
+ </div>
1533
+ <button id="analyze-button">Analyze Repository</button>
1534
+ </div>
1535
+
1536
+ <div id="analysis-result" style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; display: none;"></div>
1537
+ </div>
1538
+
1539
+ <div class="tab-content" id="text-tab">
1540
+ <div id="chat-container" class="chat-container"></div>
1541
+ <div class="chat-input">
1542
+ <input type="text" id="text-question" placeholder="Ask a question about the repository...">
1543
+ <button id="ask-button">Ask</button>
1544
+ </div>
1545
+ </div>
1546
+
1547
+ <div class="tab-content" id="voice-tab">
1548
+ <div class="controls">
1549
+ <div class="input-group">
1550
+ <label for="voice">Voice</label>
1551
+ <select id="voice">
1552
+ <option value="Puck">Puck</option>
1553
+ <option value="Charon">Charon</option>
1554
+ <option value="Kore">Kore</option>
1555
+ <option value="Fenrir">Fenrir</option>
1556
+ <option value="Aoede">Aoede</option>
1557
+ </select>
1558
+ </div>
1559
+ </div>
1560
+
1561
+ <div class="wave-container">
1562
+ <div class="box-container">
1563
+ <!-- Boxes will be dynamically added here -->
1564
+ </div>
1565
+ </div>
1566
+
1567
+ <button id="start-button">Start Voice Chat</button>
1568
+ </div>
1569
+ </div>
1570
+
1571
+ <audio id="audio-output"></audio>
1572
+
1573
+ <script>
1574
+ // Global variables
1575
+ let peerConnection;
1576
+ let audioContext;
1577
+ let dataChannel;
1578
+ let isRecording = false;
1579
+ let webrtc_id;
1580
+ let currentTab = 'repo';
1581
+ let repositoryAnalyzed = false;
1582
+
1583
+ // DOM elements
1584
+ const startButton = document.getElementById('start-button');
1585
+ const analyzeButton = document.getElementById('analyze-button');
1586
+ const askButton = document.getElementById('ask-button');
1587
+ const repoUrlInput = document.getElementById('repo-url');
1588
+ const githubTokenInput = document.getElementById('github-token');
1589
+ const geminiApiKeyInput = document.getElementById('gemini-api-key');
1590
+ const voiceSelect = document.getElementById('voice');
1591
+ const audioOutput = document.getElementById('audio-output');
1592
+ const boxContainer = document.querySelector('.box-container');
1593
+ const tabs = document.querySelectorAll('.tab');
1594
+ const tabContents = document.querySelectorAll('.tab-content');
1595
+ const chatContainer = document.getElementById('chat-container');
1596
+ const textQuestionInput = document.getElementById('text-question');
1597
+ const analysisResult = document.getElementById('analysis-result');
1598
+
1599
+ // Initialize audio visualization
1600
+ const numBars = 32;
1601
+ for (let i = 0; i < numBars; i++) {
1602
+ const box = document.createElement('div');
1603
+ box.className = 'box';
1604
+ boxContainer.appendChild(box);
1605
+ }
1606
+
1607
+ // Tab switching
1608
+ tabs.forEach(tab => {
1609
+ tab.addEventListener('click', () => {
1610
+ // First check if repository has been analyzed when switching to chat tabs
1611
+ if ((tab.dataset.tab === 'text' || tab.dataset.tab === 'voice') && !repositoryAnalyzed) {
1612
+ showError('Please analyze a repository first before chatting.');
1613
+ return;
1614
+ }
1615
+
1616
+ // Deactivate all tabs
1617
+ tabs.forEach(t => t.classList.remove('active'));
1618
+ tabContents.forEach(c => c.classList.remove('active'));
1619
+
1620
+ // Activate selected tab
1621
+ tab.classList.add('active');
1622
+ document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active');
1623
+ currentTab = tab.dataset.tab;
1624
+ });
1625
+ });
1626
+
1627
+ // Error message display
1628
+ function showError(message) {
1629
+ const toast = document.getElementById('error-toast');
1630
+ toast.textContent = message;
1631
+ toast.className = 'toast error';
1632
+ toast.style.display = 'block';
1633
+
1634
+ // Hide toast after 5 seconds
1635
+ setTimeout(() => {
1636
+ toast.style.display = 'none';
1637
+ }, 5000);
1638
+ }
1639
+
1640
+ // Repository analysis
1641
+ analyzeButton.addEventListener('click', async () => {
1642
+ const repoUrl = repoUrlInput.value.trim();
1643
+ const githubToken = githubTokenInput.value.trim();
1644
+ const geminiApiKey = geminiApiKeyInput.value.trim();
1645
+
1646
+ if (!repoUrl || !githubToken || !geminiApiKey) {
1647
+ showError('Please fill in all fields');
1648
+ return;
1649
+ }
1650
+
1651
+ try {
1652
+ analyzeButton.disabled = true;
1653
+ analyzeButton.textContent = 'Analyzing...';
1654
+
1655
+ // Call the analyze endpoint
1656
+ const response = await fetch('/analyze_repository', {
1657
+ method: 'POST',
1658
+ headers: {
1659
+ 'Content-Type': 'application/json',
1660
+ },
1661
+ body: JSON.stringify({
1662
+ repo_url: repoUrl,
1663
+ github_token: githubToken,
1664
+ gemini_api_key: geminiApiKey
1665
+ })
1666
+ });
1667
+
1668
+ const data = await response.json();
1669
+
1670
+ if (data.status === 'success') {
1671
+ // Display the analysis result
1672
+ analysisResult.textContent = data.summary;
1673
+ analysisResult.style.display = 'block';
1674
+ repositoryAnalyzed = true;
1675
+ } else {
1676
+ showError(data.message || 'Error analyzing repository');
1677
+ }
1678
+ } catch (err) {
1679
+ showError('Failed to analyze repository: ' + err.message);
1680
+ } finally {
1681
+ analyzeButton.disabled = false;
1682
+ analyzeButton.textContent = 'Analyze Repository';
1683
+ }
1684
+ });
1685
+
1686
+ // Text chat
1687
+ askButton.addEventListener('click', async () => {
1688
+ const question = textQuestionInput.value.trim();
1689
+
1690
+ if (!question) {
1691
+ return;
1692
+ }
1693
+
1694
+ // Add user message to chat
1695
+ addMessageToChat('You', question);
1696
+ textQuestionInput.value = '';
1697
+
1698
+ try {
1699
+ // Call the ask endpoint
1700
+ const response = await fetch('/ask_question', {
1701
+ method: 'POST',
1702
+ headers: {
1703
+ 'Content-Type': 'application/json',
1704
+ },
1705
+ body: JSON.stringify({
1706
+ question: question
1707
+ })
1708
+ });
1709
+
1710
+ const data = await response.json();
1711
+
1712
+ if (data.status === 'success') {
1713
+ // Add bot response to chat
1714
+ addMessageToChat('AI', data.answer);
1715
+ } else {
1716
+ addMessageToChat('AI', 'Error: ' + (data.message || 'Failed to get response'));
1717
+ }
1718
+ } catch (err) {
1719
+ addMessageToChat('AI', 'Error: ' + err.message);
1720
+ }
1721
+ });
1722
+
1723
+ // Add message to chat container
1724
+ function addMessageToChat(sender, message) {
1725
+ const messageDiv = document.createElement('div');
1726
+ messageDiv.className = 'chat-message';
1727
+
1728
+ const senderDiv = document.createElement('div');
1729
+ senderDiv.className = sender === 'You' ? 'user' : 'bot';
1730
+ senderDiv.textContent = sender + ':';
1731
+
1732
+ const contentDiv = document.createElement('div');
1733
+ contentDiv.innerHTML = formatMessage(message);
1734
+
1735
+ messageDiv.appendChild(senderDiv);
1736
+ messageDiv.appendChild(contentDiv);
1737
+
1738
+ chatContainer.appendChild(messageDiv);
1739
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1740
+ }
1741
+
1742
+ // Format message with Markdown-like syntax
1743
+ function formatMessage(message) {
1744
+ // Convert code blocks
1745
+ message = message.replace(/```([^`]+)```/g, '<pre><code>$1</code></pre>');
1746
+
1747
+ // Convert inline code
1748
+ message = message.replace(/`([^`]+)`/g, '<code>$1</code>');
1749
+
1750
+ // Convert bold
1751
+ message = message.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1752
+
1753
+ // Convert bullets
1754
+ message = message.replace(/- ([^\n]+)/g, '• $1<br>');
1755
+
1756
+ // Convert line breaks
1757
+ message = message.replace(/\n/g, '<br>');
1758
+
1759
+ return message;
1760
+ }
1761
+
1762
+ // Voice chat WebRTC setup
1763
+ async function setupWebRTC() {
1764
+ const config = __RTC_CONFIGURATION__;
1765
+ peerConnection = new RTCPeerConnection(config);
1766
+ webrtc_id = Math.random().toString(36).substring(7);
1767
+
1768
+ const timeoutId = setTimeout(() => {
1769
+ const toast = document.getElementById('error-toast');
1770
+ toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
1771
+ toast.className = 'toast warning';
1772
+ toast.style.display = 'block';
1773
+
1774
+ // Hide warning after 5 seconds
1775
+ setTimeout(() => {
1776
+ toast.style.display = 'none';
1777
+ }, 5000);
1778
+ }, 5000);
1779
+
1780
+ try {
1781
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1782
+ stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
1783
+
1784
+ // Update audio visualization setup
1785
+ audioContext = new AudioContext();
1786
+ analyser_input = audioContext.createAnalyser();
1787
+ const source = audioContext.createMediaStreamSource(stream);
1788
+ source.connect(analyser_input);
1789
+ analyser_input.fftSize = 64;
1790
+ dataArray_input = new Uint8Array(analyser_input.frequencyBinCount);
1791
+
1792
+ function updateAudioLevel() {
1793
+ analyser_input.getByteFrequencyData(dataArray_input);
1794
+ const average = Array.from(dataArray_input).reduce((a, b) => a + b, 0) / dataArray_input.length;
1795
+ const audioLevel = average / 255;
1796
+
1797
+ const pulseCircle = document.querySelector('.pulse-circle');
1798
+ if (pulseCircle) {
1799
+ pulseCircle.style.setProperty('--audio-level', 1 + audioLevel);
1800
+ }
1801
+
1802
+ animationId = requestAnimationFrame(updateAudioLevel);
1803
+ }
1804
+ updateAudioLevel();
1805
+
1806
+ // Add connection state change listener
1807
+ peerConnection.addEventListener('connectionstatechange', () => {
1808
+ console.log('connectionstatechange', peerConnection.connectionState);
1809
+ if (peerConnection.connectionState === 'connected') {
1810
+ clearTimeout(timeoutId);
1811
+ const toast = document.getElementById('error-toast');
1812
+ toast.style.display = 'none';
1813
+ }
1814
+ updateButtonState();
1815
+ });
1816
+
1817
+ // Handle incoming audio
1818
+ peerConnection.addEventListener('track', (evt) => {
1819
+ if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
1820
+ audioOutput.srcObject = evt.streams[0];
1821
+ audioOutput.play();
1822
+
1823
+ // Set up audio visualization on the output stream
1824
+ audioContext = new AudioContext();
1825
+ analyser = audioContext.createAnalyser();
1826
+ const source = audioContext.createMediaStreamSource(evt.streams[0]);
1827
+ source.connect(analyser);
1828
+ analyser.fftSize = 2048;
1829
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
1830
+ updateVisualization();
1831
+ }
1832
+ });
1833
+
1834
+ // Create data channel for messages
1835
+ dataChannel = peerConnection.createDataChannel('text');
1836
+ dataChannel.onmessage = (event) => {
1837
+ const eventJson = JSON.parse(event.data);
1838
+ if (eventJson.type === "error") {
1839
+ showError(eventJson.message);
1840
+ } else if (eventJson.type === "send_input") {
1841
+ fetch('/input_hook', {
1842
+ method: 'POST',
1843
+ headers: {
1844
+ 'Content-Type': 'application/json',
1845
+ },
1846
+ body: JSON.stringify({
1847
+ webrtc_id: webrtc_id,
1848
+ api_key: geminiApiKeyInput.value,
1849
+ voice_name: voiceSelect.value,
1850
+ repo_url: repoUrlInput.value,
1851
+ github_token: githubTokenInput.value
1852
+ })
1853
+ });
1854
+ }
1855
+ };
1856
+
1857
+ // Create and send offer
1858
+ const offer = await peerConnection.createOffer();
1859
+ await peerConnection.setLocalDescription(offer);
1860
+
1861
+ await new Promise((resolve) => {
1862
+ if (peerConnection.iceGatheringState === "complete") {
1863
+ resolve();
1864
+ } else {
1865
+ const checkState = () => {
1866
+ if (peerConnection.iceGatheringState === "complete") {
1867
+ peerConnection.removeEventListener("icegatheringstatechange", checkState);
1868
+ resolve();
1869
+ }
1870
+ };
1871
+ peerConnection.addEventListener("icegatheringstatechange", checkState);
1872
+ }
1873
+ });
1874
+
1875
+ const response = await fetch('/webrtc/offer', {
1876
+ method: 'POST',
1877
+ headers: { 'Content-Type': 'application/json' },
1878
+ body: JSON.stringify({
1879
+ sdp: peerConnection.localDescription.sdp,
1880
+ type: peerConnection.localDescription.type,
1881
+ webrtc_id: webrtc_id,
1882
+ })
1883
+ });
1884
+
1885
+ const serverResponse = await response.json();
1886
+
1887
+ if (serverResponse.status === 'failed') {
1888
+ showError(serverResponse.meta.error === 'concurrency_limit_reached'
1889
+ ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
1890
+ : serverResponse.meta.error);
1891
+ stop();
1892
+ startButton.textContent = 'Start Voice Chat';
1893
+ return;
1894
+ }
1895
+
1896
+ await peerConnection.setRemoteDescription(serverResponse);
1897
+ } catch (err) {
1898
+ clearTimeout(timeoutId);
1899
+ console.error('Error setting up WebRTC:', err);
1900
+ showError('Failed to establish connection. Please try again.');
1901
+ stop();
1902
+ startButton.textContent = 'Start Voice Chat';
1903
+ }
1904
+ }
1905
+
1906
+ function updateButtonState() {
1907
+ if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
1908
+ startButton.innerHTML = `
1909
+ <div class="icon-with-spinner">
1910
+ <div class="spinner"></div>
1911
+ <span>Connecting...</span>
1912
+ </div>
1913
+ `;
1914
+ } else if (peerConnection && peerConnection.connectionState === 'connected') {
1915
+ startButton.innerHTML = `
1916
+ <div class="pulse-container">
1917
+ <div class="pulse-circle"></div>
1918
+ <span>Stop Voice Chat</span>
1919
+ </div>
1920
+ `;
1921
+ } else {
1922
+ startButton.innerHTML = 'Start Voice Chat';
1923
+ }
1924
+ }
1925
+
1926
+ function updateVisualization() {
1927
+ if (!analyser) return;
1928
+
1929
+ analyser.getByteFrequencyData(dataArray);
1930
+ const bars = document.querySelectorAll('.box');
1931
+
1932
+ for (let i = 0; i < bars.length; i++) {
1933
+ const barHeight = (dataArray[i] / 255) * 2;
1934
+ bars[i].style.transform = `scaleY(${Math.max(0.1, barHeight)})`;
1935
+ }
1936
+
1937
+ animationId = requestAnimationFrame(updateVisualization);
1938
+ }
1939
+
1940
+ function stopWebRTC() {
1941
+ if (peerConnection) {
1942
+ peerConnection.close();
1943
+ }
1944
+ if (animationId) {
1945
+ cancelAnimationFrame(animationId);
1946
+ }
1947
+ if (audioContext) {
1948
+ audioContext.close();
1949
+ }
1950
+ updateButtonState();
1951
+ }
1952
+
1953
+ startButton.addEventListener('click', () => {
1954
+ if (!isRecording) {
1955
+ setupWebRTC();
1956
+ startButton.classList.add('recording');
1957
+ } else {
1958
+ stopWebRTC();
1959
+ startButton.classList.remove('recording');
1960
+ }
1961
+ isRecording = !isRecording;
1962
+ });
1963
+
1964
+ // Press Enter to send text chat message
1965
+ textQuestionInput.addEventListener('keypress', (e) => {
1966
+ if (e.key === 'Enter') {
1967
+ askButton.click();
1968
+ }
1969
+ });
1970
+ </script>
1971
+ </body>
1972
+
1973
+ </html>
1974
+ """
1975
+ else:
1976
+ html_content = index_html_path.read_text()
1977
+
1978
+ # Replace RTC configuration
1979
+ html_content = html_content.replace("__RTC_CONFIGURATION__", json.dumps(rtc_config))
1980
+ return HTMLResponse(content=html_content)
1981
+
1982
+
1983
+ # Gradio interface for web UI
1984
+ def create_gradio_app():
1985
+ with gr.Blocks(theme=gr.themes.Soft()) as app:
1986
+ gr.Markdown("""
1987
+ # 🔍 GitHub Repository Analyzer with Voice Chat
1988
+
1989
+ Analyze any public GitHub repository using AI. The tool will:
1990
+ 1. 📊 Analyze repository structure, code, and development patterns
1991
+ 2. 💡 Generate comprehensive insights about the repository
1992
+ 3. 💬 Allow you to chat with AI about the repository using text or voice
1993
+ 4. 📁 Search code and view files in the repository
1994
+
1995
+ Enter a GitHub repository URL and your API keys to get started.
1996
+ """)
1997
+
1998
+ with gr.Row():
1999
+ with gr.Column():
2000
+ repo_url = gr.Textbox(
2001
+ label="GitHub Repository URL",
2002
+ placeholder="https://github.com/owner/repo",
2003
+ value=""
2004
+ )
2005
+ github_token = gr.Textbox(
2006
+ label="GitHub API Token",
2007
+ placeholder="Your GitHub API Token",
2008
+ type="password",
2009
+ value=GITHUB_TOKEN if GITHUB_TOKEN != "YOUR_GITHUB_TOKEN" else ""
2010
+ )
2011
+ gemini_api_key = gr.Textbox(
2012
+ label="Gemini API Key",
2013
+ placeholder="Your Gemini API Key",
2014
+ type="password",
2015
+ value=GEMINI_API_KEY if GEMINI_API_KEY != "YOUR_GEMINI_API_KEY" else ""
2016
+ )
2017
+
2018
+ with gr.Row():
2019
+ analyze_btn = gr.Button("🔍 Analyze Repository", variant="primary")
2020
+
2021
+ # Add status message
2022
+ status_msg = gr.Markdown("", elem_id="status_message")
2023
+
2024
+ tabs = gr.Tabs()
2025
+ with tabs:
2026
+ with gr.TabItem("Analysis"):
2027
+ # Use Markdown for better formatting
2028
+ summary = gr.Markdown(
2029
+ label="Analysis Summary",
2030
+ value=""
2031
+ )
2032
+
2033
+ with gr.TabItem("Text Chat"):
2034
+ repo_chatbot = gr.Chatbot(
2035
+ label="Chat with Repository",
2036
+ height=400,
2037
+ show_label=True
2038
+ )
2039
+
2040
+ with gr.Row():
2041
+ repo_question = gr.Textbox(
2042
+ label="Ask about the repository",
2043
+ placeholder="Ask a question, search code, or request a file...",
2044
+ scale=4
2045
+ )
2046
+ repo_ask_btn = gr.Button("💬 Ask", variant="primary", scale=1)
2047
+ repo_clear_btn = gr.Button("🗑️ Clear Chat", variant="secondary", scale=1)
2048
+
2049
+ with gr.TabItem("Voice Chat"):
2050
+ gr.Markdown("""
2051
+ ## Voice Chat with Repository
2052
+
2053
+ Speak with an AI assistant about the repository using real-time voice chat.
2054
+ Click the "Open Voice Chat" button to launch the voice interface in a new tab.
2055
+
2056
+ Note: Voice chat requires browser permission to access your microphone.
2057
+ """)
2058
+
2059
+ voice_chat_btn = gr.Button("🎤 Open Voice Chat", variant="primary")
2060
+
2061
+ # Hidden state for analysis file and analyzer
2062
+ analysis_file = gr.State("")
2063
+ analyzer_state = gr.State(None)
2064
+ system_prompt_state = gr.State("")
2065
+
2066
+ def clear_chat():
2067
+ return []
2068
+
2069
+ def clear_outputs():
2070
+ return "", [], "", None, ""
2071
+
2072
+ def open_voice_chat():
2073
+ return "Opening voice chat in a new tab...", gr.update()
2074
+
2075
+ # Set up event handlers
2076
+ analyze_btn.click(
2077
+ fn=lambda: "⏳ Analysis in progress...",
2078
+ inputs=None,
2079
+ outputs=status_msg,
2080
+ queue=False
2081
+ ).then(
2082
+ analyze_repository,
2083
+ inputs=[repo_url, github_token, gemini_api_key],
2084
+ outputs=[summary, analysis_file, analyzer_state, system_prompt_state],
2085
+ ).then(
2086
+ lambda: "✅ Analysis complete! You can now chat with the repository.",
2087
+ inputs=None,
2088
+ outputs=status_msg,
2089
+ queue=False
2090
+ )
2091
+
2092
+ repo_ask_btn.click(
2093
+ ask_question,
2094
+ inputs=[repo_question, analysis_file, analyzer_state, gemini_api_key, repo_chatbot],
2095
+ outputs=[repo_chatbot],
2096
+ ).then(
2097
+ lambda: "", # Clear the question input
2098
+ None,
2099
+ repo_question,
2100
+ queue=False
2101
+ )
2102
+
2103
+ repo_clear_btn.click(
2104
+ clear_chat,
2105
+ inputs=None,
2106
+ outputs=[repo_chatbot],
2107
+ queue=False
2108
+ )
2109
+
2110
+ voice_chat_btn.click(
2111
+ open_voice_chat,
2112
+ inputs=None,
2113
+ outputs=[status_msg, voice_chat_btn],
2114
+ ).then(
2115
+ lambda: gr.update(value="Voice Chat Opened"),
2116
+ None,
2117
+ voice_chat_btn,
2118
+ js="""() => { window.open('/', '_blank'); return null; }"""
2119
+ )
2120
+
2121
+ return app
2122
+
2123
+ # Main execution
2124
+ if __name__ == "__main__":
2125
+ import uvicorn
2126
+
2127
+ # Create Gradio interface
2128
+ gradio_app = create_gradio_app()
2129
+
2130
+ # Get the Gradio FastAPI app
2131
+ gradio_app = gr.mount_gradio_app(app, gradio_app, path="/gradio")
2132
+
2133
+ # Start the FastAPI server
2134
+ uvicorn.run(app, host="0.0.0.0", port=7860)
index-html.html ADDED
@@ -0,0 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>GitHub Repository Voice Chat</title>
8
+ <style>
9
+ :root {
10
+ --color-accent: #6366f1;
11
+ --color-background: #0f172a;
12
+ --color-surface: #1e293b;
13
+ --color-text: #e2e8f0;
14
+ --boxSize: 8px;
15
+ --gutter: 4px;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ padding: 0;
21
+ background-color: var(--color-background);
22
+ color: var(--color-text);
23
+ font-family: system-ui, -apple-system, sans-serif;
24
+ min-height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center;
28
+ justify-content: center;
29
+ }
30
+
31
+ .container {
32
+ width: 90%;
33
+ max-width: 800px;
34
+ background-color: var(--color-surface);
35
+ padding: 2rem;
36
+ border-radius: 1rem;
37
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
38
+ }
39
+
40
+ .wave-container {
41
+ position: relative;
42
+ display: flex;
43
+ min-height: 100px;
44
+ max-height: 128px;
45
+ justify-content: center;
46
+ align-items: center;
47
+ margin: 2rem 0;
48
+ }
49
+
50
+ .box-container {
51
+ display: flex;
52
+ justify-content: space-between;
53
+ height: 64px;
54
+ width: 100%;
55
+ }
56
+
57
+ .box {
58
+ height: 100%;
59
+ width: var(--boxSize);
60
+ background: var(--color-accent);
61
+ border-radius: 8px;
62
+ transition: transform 0.05s ease;
63
+ }
64
+
65
+ .controls {
66
+ display: grid;
67
+ gap: 1rem;
68
+ margin-bottom: 2rem;
69
+ }
70
+
71
+ .input-group {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 0.5rem;
75
+ }
76
+
77
+ label {
78
+ font-size: 0.875rem;
79
+ font-weight: 500;
80
+ }
81
+
82
+ input,
83
+ select {
84
+ padding: 0.75rem;
85
+ border-radius: 0.5rem;
86
+ border: 1px solid rgba(255, 255, 255, 0.1);
87
+ background-color: var(--color-background);
88
+ color: var(--color-text);
89
+ font-size: 1rem;
90
+ }
91
+
92
+ button {
93
+ padding: 1rem 2rem;
94
+ border-radius: 0.5rem;
95
+ border: none;
96
+ background-color: var(--color-accent);
97
+ color: white;
98
+ font-weight: 600;
99
+ cursor: pointer;
100
+ transition: all 0.2s ease;
101
+ }
102
+
103
+ button:hover {
104
+ opacity: 0.9;
105
+ transform: translateY(-1px);
106
+ }
107
+
108
+ .icon-with-spinner {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: 12px;
113
+ min-width: 180px;
114
+ }
115
+
116
+ .spinner {
117
+ width: 20px;
118
+ height: 20px;
119
+ border: 2px solid white;
120
+ border-top-color: transparent;
121
+ border-radius: 50%;
122
+ animation: spin 1s linear infinite;
123
+ flex-shrink: 0;
124
+ }
125
+
126
+ @keyframes spin {
127
+ to {
128
+ transform: rotate(360deg);
129
+ }
130
+ }
131
+
132
+ .pulse-container {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ gap: 12px;
137
+ min-width: 180px;
138
+ }
139
+
140
+ .pulse-circle {
141
+ width: 20px;
142
+ height: 20px;
143
+ border-radius: 50%;
144
+ background-color: white;
145
+ opacity: 0.2;
146
+ flex-shrink: 0;
147
+ transform: translateX(-0%) scale(var(--audio-level, 1));
148
+ transition: transform 0.1s ease;
149
+ }
150
+
151
+ /* Add styles for toast notifications */
152
+ .toast {
153
+ position: fixed;
154
+ top: 20px;
155
+ left: 50%;
156
+ transform: translateX(-50%);
157
+ padding: 16px 24px;
158
+ border-radius: 4px;
159
+ font-size: 14px;
160
+ z-index: 1000;
161
+ display: none;
162
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
163
+ }
164
+
165
+ .toast.error {
166
+ background-color: #f44336;
167
+ color: white;
168
+ }
169
+
170
+ .toast.warning {
171
+ background-color: #ffd700;
172
+ color: black;
173
+ }
174
+
175
+ /* Repository input section */
176
+ .repo-section {
177
+ margin-bottom: 2rem;
178
+ padding-bottom: 2rem;
179
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
180
+ }
181
+
182
+ /* Conversation section */
183
+ .conversation {
184
+ background-color: rgba(0, 0, 0, 0.2);
185
+ border-radius: 0.5rem;
186
+ padding: 1rem;
187
+ min-height: 200px;
188
+ max-height: 300px;
189
+ overflow-y: auto;
190
+ margin-bottom: 1rem;
191
+ }
192
+
193
+ .message {
194
+ margin-bottom: 1rem;
195
+ padding: 0.5rem 1rem;
196
+ border-radius: 0.5rem;
197
+ }
198
+
199
+ .user-message {
200
+ background-color: rgba(99, 102, 241, 0.2);
201
+ margin-left: 2rem;
202
+ }
203
+
204
+ .bot-message {
205
+ background-color: rgba(16, 185, 129, 0.2);
206
+ margin-right: 2rem;
207
+ }
208
+ </style>
209
+ </head>
210
+
211
+
212
+ <body>
213
+ <!-- Add toast element after body opening tag -->
214
+ <div id="error-toast" class="toast"></div>
215
+ <div style="text-align: center">
216
+ <h1>GitHub Repository Voice Chat</h1>
217
+ <p>Talk to AI about a GitHub repository using real-time voice</p>
218
+ </div>
219
+ <div class="container">
220
+ <div class="repo-section">
221
+ <h2>Repository Information</h2>
222
+ <div class="controls">
223
+ <div class="input-group">
224
+ <label for="repo-url">GitHub Repository URL</label>
225
+ <input type="text" id="repo-url" placeholder="https://github.com/owner/repo">
226
+ </div>
227
+ <div class="input-group">
228
+ <label for="github-token">GitHub API Token</label>
229
+ <input type="password" id="github-token" placeholder="Your GitHub API token">
230
+ </div>
231
+ <button id="analyze-button">Analyze Repository</button>
232
+ </div>
233
+ <div id="repo-status"></div>
234
+ </div>
235
+
236
+ <h2>Voice Chat</h2>
237
+ <div class="controls">
238
+ <div class="input-group">
239
+ <label for="api-key">Gemini API Key</label>
240
+ <input type="password" id="api-key" placeholder="Enter your Gemini API key">
241
+ </div>
242
+ <div class="input-group">
243
+ <label for="voice">Voice</label>
244
+ <select id="voice">
245
+ <option value="Puck">Puck</option>
246
+ <option value="Charon">Charon</option>
247
+ <option value="Kore">Kore</option>
248
+ <option value="Fenrir">Fenrir</option>
249
+ <option value="Aoede">Aoede</option>
250
+ </select>
251
+ </div>
252
+ </div>
253
+
254
+ <div class="conversation" id="conversation">
255
+ <div class="message bot-message">
256
+ Hello! I'm your GitHub repository assistant. Once you analyze a repository using the form above,
257
+ you can ask me questions about it using your voice. Click the Start Recording button below to begin.
258
+ </div>
259
+ </div>
260
+
261
+ <div class="wave-container">
262
+ <div class="box-container">
263
+ <!-- Boxes will be dynamically added here -->
264
+ </div>
265
+ </div>
266
+
267
+ <button id="start-button">Start Recording</button>
268
+ </div>
269
+
270
+ <audio id="audio-output"></audio>
271
+
272
+ <script>
273
+ let peerConnection;
274
+ let audioContext;
275
+ let dataChannel;
276
+ let isRecording = false;
277
+ let webrtc_id;
278
+ let repositoryAnalyzed = false;
279
+
280
+ const startButton = document.getElementById('start-button');
281
+ const analyzeButton = document.getElementById('analyze-button');
282
+ const apiKeyInput = document.getElementById('api-key');
283
+ const voiceSelect = document.getElementById('voice');
284
+ const repoUrlInput = document.getElementById('repo-url');
285
+ const githubTokenInput = document.getElementById('github-token');
286
+ const repoStatus = document.getElementById('repo-status');
287
+ const audioOutput = document.getElementById('audio-output');
288
+ const boxContainer = document.querySelector('.box-container');
289
+ const conversation = document.getElementById('conversation');
290
+
291
+ const numBars = 32;
292
+ for (let i = 0; i < numBars; i++) {
293
+ const box = document.createElement('div');
294
+ box.className = 'box';
295
+ boxContainer.appendChild(box);
296
+ }
297
+
298
+ function updateButtonState() {
299
+ if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
300
+ startButton.innerHTML = `
301
+ <div class="icon-with-spinner">
302
+ <div class="spinner"></div>
303
+ <span>Connecting...</span>
304
+ </div>
305
+ `;
306
+ } else if (peerConnection && peerConnection.connectionState === 'connected') {
307
+ startButton.innerHTML = `
308
+ <div class="pulse-container">
309
+ <div class="pulse-circle"></div>
310
+ <span>Stop Recording</span>
311
+ </div>
312
+ `;
313
+ } else {
314
+ startButton.innerHTML = 'Start Recording';
315
+ }
316
+ }
317
+
318
+ function showError(message) {
319
+ const toast = document.getElementById('error-toast');
320
+ toast.textContent = message;
321
+ toast.className = 'toast error';
322
+ toast.style.display = 'block';
323
+
324
+ // Hide toast after 5 seconds
325
+ setTimeout(() => {
326
+ toast.style.display = 'none';
327
+ }, 5000);
328
+ }
329
+
330
+ // Add a message to the conversation
331
+ function addMessage(text, isUser = false) {
332
+ const messageDiv = document.createElement('div');
333
+ messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
334
+ messageDiv.textContent = text;
335
+ conversation.appendChild(messageDiv);
336
+ conversation.scrollTop = conversation.scrollHeight;
337
+ }
338
+
339
+ // Analyze repository
340
+ analyzeButton.addEventListener('click', async () => {
341
+ const repoUrl = repoUrlInput.value.trim();
342
+ const githubToken = githubTokenInput.value.trim();
343
+
344
+ if (!repoUrl || !githubToken) {
345
+ showError('Please enter both repository URL and GitHub token');
346
+ return;
347
+ }
348
+
349
+ repoStatus.textContent = 'Analyzing repository...';
350
+ analyzeButton.disabled = true;
351
+
352
+ try {
353
+ const response = await fetch('/analyze_repository', {
354
+ method: 'POST',
355
+ headers: { 'Content-Type': 'application/json' },
356
+ body: JSON.stringify({
357
+ repo_url: repoUrl,
358
+ github_token: githubToken,
359
+ gemini_api_key: apiKeyInput.value
360
+ })
361
+ });
362
+
363
+ const result = await response.json();
364
+
365
+ if (result.status === 'success') {
366
+ repoStatus.textContent = 'Repository analyzed successfully! You can now start voice chat.';
367
+ repoStatus.style.color = '#10b981';
368
+ repositoryAnalyzed = true;
369
+
370
+ // Add message to conversation
371
+ addMessage('I\'ve analyzed the repository. You can ask me questions about it now!', false);
372
+ } else {
373
+ repoStatus.textContent = 'Error analyzing repository: ' + result.message;
374
+ repoStatus.style.color = '#f44336';
375
+ }
376
+ } catch (err) {
377
+ repoStatus.textContent = 'Error: ' + err.message;
378
+ repoStatus.style.color = '#f44336';
379
+ } finally {
380
+ analyzeButton.disabled = false;
381
+ }
382
+ });
383
+
384
+ async function setupWebRTC() {
385
+ const config = __RTC_CONFIGURATION__;
386
+ peerConnection = new RTCPeerConnection(config);
387
+ webrtc_id = Math.random().toString(36).substring(7);
388
+
389
+ const timeoutId = setTimeout(() => {
390
+ const toast = document.getElementById('error-toast');
391
+ toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
392
+ toast.className = 'toast warning';
393
+ toast.style.display = 'block';
394
+
395
+ // Hide warning after 5 seconds
396
+ setTimeout(() => {
397
+ toast.style.display = 'none';
398
+ }, 5000);
399
+ }, 5000);
400
+
401
+ try {
402
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
403
+ stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
404
+
405
+ // Update audio visualization setup
406
+ audioContext = new AudioContext();
407
+ analyser_input = audioContext.createAnalyser();
408
+ const source = audioContext.createMediaStreamSource(stream);
409
+ source.connect(analyser_input);
410
+ analyser_input.fftSize = 64;
411
+ dataArray_input = new Uint8Array(analyser_input.frequencyBinCount);
412
+
413
+ function updateAudioLevel() {
414
+ analyser_input.getByteFrequencyData(dataArray_input);
415
+ const average = Array.from(dataArray_input).reduce((a, b) => a + b, 0) / dataArray_input.length;
416
+ const audioLevel = average / 255;
417
+
418
+ const pulseCircle = document.querySelector('.pulse-circle');
419
+ if (pulseCircle) {
420
+ console.log("audioLevel", audioLevel);
421
+ pulseCircle.style.setProperty('--audio-level', 1 + audioLevel);
422
+ }
423
+
424
+ animationId = requestAnimationFrame(updateAudioLevel);
425
+ }
426
+ updateAudioLevel();
427
+
428
+ // Add connection state change listener
429
+ peerConnection.addEventListener('connectionstatechange', () => {
430
+ console.log('connectionstatechange', peerConnection.connectionState);
431
+ if (peerConnection.connectionState === 'connected') {
432
+ clearTimeout(timeoutId);
433
+ const toast = document.getElementById('error-toast');
434
+ toast.style.display = 'none';
435
+
436
+ // Add a user message indicating voice is active
437
+ addMessage('Voice chat connected. You can speak now.', false);
438
+ }
439
+ updateButtonState();
440
+ });
441
+
442
+ // Handle incoming audio
443
+ peerConnection.addEventListener('track', (evt) => {
444
+ if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
445
+ audioOutput.srcObject = evt.streams[0];
446
+ audioOutput.play();
447
+
448
+ // Set up audio visualization on the output stream
449
+ audioContext = new AudioContext();
450
+ analyser = audioContext.createAnalyser();
451
+ const source = audioContext.createMediaStreamSource(evt.streams[0]);
452
+ source.connect(analyser);
453
+ analyser.fftSize = 2048;
454
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
455
+ updateVisualization();
456
+ }
457
+ });
458
+
459
+ // Create data channel for messages
460
+ dataChannel = peerConnection.createDataChannel('text');
461
+ dataChannel.onmessage = (event) => {
462
+ const eventJson = JSON.parse(event.data);
463
+ if (eventJson.type === "error") {
464
+ showError(eventJson.message);
465
+ } else if (eventJson.type === "send_input") {
466
+ fetch('/input_hook', {
467
+ method: 'POST',
468
+ headers: {
469
+ 'Content-Type': 'application/json',
470
+ },
471
+ body: JSON.stringify({
472
+ webrtc_id: webrtc_id,
473
+ api_key: apiKeyInput.value,
474
+ voice_name: voiceSelect.value,
475
+ repo_url: repoUrlInput.value,
476
+ github_token: githubTokenInput.value
477
+ })
478
+ });
479
+ }
480
+ };
481
+
482
+ // Create and send offer
483
+ const offer = await peerConnection.createOffer();
484
+ await peerConnection.setLocalDescription(offer);
485
+
486
+ await new Promise((resolve) => {
487
+ if (peerConnection.iceGatheringState === "complete") {
488
+ resolve();
489
+ } else {
490
+ const checkState = () => {
491
+ if (peerConnection.iceGatheringState === "complete") {
492
+ peerConnection.removeEventListener("icegatheringstatechange", checkState);
493
+ resolve();
494
+ }
495
+ };
496
+ peerConnection.addEventListener("icegatheringstatechange", checkState);
497
+ }
498
+ });
499
+
500
+ const response = await fetch('/webrtc/offer', {
501
+ method: 'POST',
502
+ headers: { 'Content-Type': 'application/json' },
503
+ body: JSON.stringify({
504
+ sdp: peerConnection.localDescription.sdp,
505
+ type: peerConnection.localDescription.type,
506
+ webrtc_id: webrtc_id,
507
+ })
508
+ });
509
+
510
+ const serverResponse = await response.json();
511
+
512
+ if (serverResponse.status === 'failed') {
513
+ showError(serverResponse.meta.error === 'concurrency_limit_reached'
514
+ ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
515
+ : serverResponse.meta.error);
516
+ stop();
517
+ startButton.textContent = 'Start Recording';
518
+ return;
519
+ }
520
+
521
+ await peerConnection.setRemoteDescription(serverResponse);
522
+ } catch (err) {
523
+ clearTimeout(timeoutId);
524
+ console.error('Error setting up WebRTC:', err);
525
+ showError('Failed to establish connection. Please try again.');
526
+ stop();
527
+ startButton.textContent = 'Start Recording';
528
+ }
529
+ }
530
+
531
+ function updateVisualization() {
532
+ if (!analyser) return;
533
+
534
+ analyser.getByteFrequencyData(dataArray);
535
+ const bars = document.querySelectorAll('.box');
536
+
537
+ for (let i = 0; i < bars.length; i++) {
538
+ const barHeight = (dataArray[i] / 255) * 2;
539
+ bars[i].style.transform = `scaleY(${Math.max(0.1, barHeight)})`;
540
+ }
541
+
542
+ animationId = requestAnimationFrame(updateVisualization);
543
+ }
544
+
545
+ function stopWebRTC() {
546
+ if (peerConnection) {
547
+ peerConnection.close();
548
+ }
549
+ if (animationId) {
550
+ cancelAnimationFrame(animationId);
551
+ }
552
+ if (audioContext) {
553
+ audioContext.close();
554
+ }
555
+ updateButtonState();
556
+
557
+ // Add message when voice chat ends
558
+ addMessage('Voice chat ended. Click "Start Recording" to chat again.', false);
559
+ }
560
+
561
+ startButton.addEventListener('click', () => {
562
+ // Check if repository has been analyzed
563
+ if (!repositoryAnalyzed) {
564
+ showError('Please analyze a repository first by entering a URL and GitHub token above.');
565
+ return;
566
+ }
567
+
568
+ // Check if API key is provided
569
+ if (!apiKeyInput.value.trim()) {
570
+ showError('Please enter a Gemini API key.');
571
+ return;
572
+ }
573
+
574
+ if (!isRecording) {
575
+ setupWebRTC();
576
+ startButton.classList.add('recording');
577
+ addMessage('Starting voice chat...', true);
578
+ } else {
579
+ stopWebRTC();
580
+ startButton.classList.remove('recording');
581
+ }
582
+ isRecording = !isRecording;
583
+ });
584
+ </script>
585
+ </body>
586
+
587
+ </html>
requirements.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ gradio==4.6.0
3
+ PyGithub==2.1.1
4
+ google-generativeai==0.7.0
5
+ python-dotenv==1.0.0
6
+ tenacity==8.2.3
7
+ requests==2.31.0
8
+
9
+ # Voice chat dependencies
10
+ fastrtc
11
+ numpy
12
+ fastapi
13
+ uvicorn
14
+ twilio
15
+
16
+ # Repository analysis dependencies
17
+ nltk
18
+ spacy
19
+ textblob
20
+ markdown
21
+ pygments
22
+ langdetect
23
+
24
+ # Optional dependencies
25
+ pydantic
26
+ aiohttp
27
+ beautifulsoup4
28
+ python-magic
29
+ chardet