Spaces:
Running
Running
init project
Browse files- PromptTemplate.json +3 -0
- Prompts/CreateApp.md +9 -0
- agents.py +483 -0
- app.py +319 -0
- github_utils.py +384 -0
- message.txt +2 -0
- requirements.txt +3 -0
- templates/cat_style.css +250 -0
- templates/index.html +1158 -0
PromptTemplate.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"prompt_template": "You are an expert repository analyst and technical educator, skilled at explaining software repository setups and configurations.\n\n# Task Description\nGenerate 3 multiple-choice questions about the given repository with 4 options each. Include explanations for the correct answers.\n\n# Output Format\n- Q1: Question 1\n- A1: Option 1\n- A2: Option 2\n- A3: Option 3\n- A4: Option 4\n- Correct Answer: 1\n- Explanation: Explanation for the correct answer\n\n# Input Content\nRepository: {content}\nCategory: {category}\nSource: {source}\n\n# Requirements\n1. Focus on repository setup, configuration, and best practices\n2. Include questions about typical file structures, commands, or configurations\n3. Each question must have exactly 4 options with only one correct answer\n4. Provide detailed explanations for why the correct answer is appropriate\n5. Make questions practical and useful for developers working with this type of repository\n6. Consider common pitfalls and configuration issues"
|
3 |
+
}
|
Prompts/CreateApp.md
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
In this task, I want to create a website with chatbot (agent). this agent Can target a repo in GitHub. After it analyze the repo, it will give a report about the repo. The report will include the following:
|
2 |
+
- the chat agent to allow users to enter a GitHub repository URL.
|
3 |
+
- analyze that specific repository.
|
4 |
+
- answer questions about it.
|
5 |
+
- Question includes not limited to:
|
6 |
+
How to setup the project.
|
7 |
+
How to run the project.
|
8 |
+
What is the project about.
|
9 |
+
What is the project structure.
|
agents.py
ADDED
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import warnings
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
|
5 |
+
warnings.filterwarnings("ignore")
|
6 |
+
from together import Together
|
7 |
+
|
8 |
+
# Load configuration from config.json
|
9 |
+
def load_config():
|
10 |
+
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
11 |
+
with open(config_path, "r") as f:
|
12 |
+
return json.load(f)
|
13 |
+
|
14 |
+
# Get API key and model from config
|
15 |
+
config = load_config()
|
16 |
+
your_api_key = config["together_ai_token"]
|
17 |
+
model = config.get("model", "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8") # Use default if not in config
|
18 |
+
client = Together(api_key=your_api_key)
|
19 |
+
|
20 |
+
|
21 |
+
def prompt_llm(prompt, show_cost=False):
|
22 |
+
# This function allows us to prompt an LLM via the Together API
|
23 |
+
|
24 |
+
# Calculate the number of tokens
|
25 |
+
tokens = len(prompt.split())
|
26 |
+
|
27 |
+
# Calculate and print estimated cost for each model
|
28 |
+
if show_cost:
|
29 |
+
print(f"\nNumber of tokens: {tokens}")
|
30 |
+
cost = (0.1 / 1_000_000) * tokens
|
31 |
+
print(f"Estimated cost for {model}: ${cost:.10f}\n")
|
32 |
+
|
33 |
+
# Make the API call
|
34 |
+
response = client.chat.completions.create(
|
35 |
+
model=model,
|
36 |
+
messages=[{"role": "user", "content": prompt}],
|
37 |
+
)
|
38 |
+
return response.choices[0].message.content
|
39 |
+
|
40 |
+
|
41 |
+
class SummarizerAgent:
|
42 |
+
def __init__(self):
|
43 |
+
self.client = Together(api_key=your_api_key)
|
44 |
+
|
45 |
+
def process(self, content):
|
46 |
+
prompt = """SYSTEM: You are an expert code summarizer.
|
47 |
+
Your task is to condense the provided code into a clear, informative summary of exactly 4 lines.
|
48 |
+
|
49 |
+
INSTRUCTIONS:
|
50 |
+
β’ Identify and include only the most important functionality
|
51 |
+
β’ Explain what the code does and its purpose
|
52 |
+
β’ Ensure the summary is exactly 4 lines long
|
53 |
+
β’ Use concise, clear language
|
54 |
+
β’ Show output only - provide just the summary
|
55 |
+
β’ Do not include any other text or comments, show only the summary
|
56 |
+
* do not say "Here is a 4-line summary: " show the summary directly and nothing else
|
57 |
+
|
58 |
+
Code to summarize: {content}
|
59 |
+
|
60 |
+
Provide a 4-line summary:"""
|
61 |
+
|
62 |
+
return prompt_llm(prompt.format(content=content))
|
63 |
+
|
64 |
+
|
65 |
+
class InsightAgent:
|
66 |
+
def __init__(self):
|
67 |
+
self.client = Together(api_key=your_api_key)
|
68 |
+
|
69 |
+
def process_text(self, summaries):
|
70 |
+
# Process a list of summary texts directly
|
71 |
+
all_summaries = "\n\n".join(summaries)
|
72 |
+
return self._generate_insights(all_summaries)
|
73 |
+
|
74 |
+
def _generate_insights(self, all_summaries):
|
75 |
+
prompt = """SYSTEM: You are an expert code analyst who can identify key insights from code summaries.
|
76 |
+
|
77 |
+
INSTRUCTIONS:
|
78 |
+
β’ Review the provided code summaries
|
79 |
+
β’ Identify 3 key insights that represent the most important takeaways
|
80 |
+
β’ Consider code structure, patterns, and best practices
|
81 |
+
β’ Format your response as exactly 3 bullet points
|
82 |
+
β’ Each bullet point must be a single sentence
|
83 |
+
β’ Be concise, clear, and informative
|
84 |
+
β’ Do not include any introductory or concluding text
|
85 |
+
β’ Show only the 3 bullet points, nothing else
|
86 |
+
|
87 |
+
Summaries to analyze:
|
88 |
+
{summaries}
|
89 |
+
|
90 |
+
Provide exactly 3 bullet point insights:"""
|
91 |
+
|
92 |
+
return prompt_llm(prompt.format(summaries=all_summaries))
|
93 |
+
|
94 |
+
|
95 |
+
class RecommenderAgent:
|
96 |
+
def __init__(self):
|
97 |
+
self.client = Together(api_key=your_api_key)
|
98 |
+
|
99 |
+
def process(self, insights, summaries, user_goal, persona=""):
|
100 |
+
prompt = """SYSTEM: You are an expert code consultant who provides actionable recommendations.
|
101 |
+
|
102 |
+
INSTRUCTIONS:
|
103 |
+
β’ Review the provided insights and summaries about the code
|
104 |
+
β’ Consider the user's specific goal: {user_goal}
|
105 |
+
β’ Consider the user's persona: {persona}
|
106 |
+
β’ Recommend exactly 2 specific, actionable steps the user can take to improve or work with this codebase
|
107 |
+
β’ Each recommendation should be practical, specific, and directly related to the goal
|
108 |
+
β’ Format your response as exactly 2 bullet points
|
109 |
+
β’ Each bullet point should be 1-2 sentences
|
110 |
+
β’ Be concise, clear, and actionable
|
111 |
+
β’ Do not include any introductory or concluding text
|
112 |
+
β’ Show only the 2 bullet point recommendations, nothing else
|
113 |
+
|
114 |
+
Insights:
|
115 |
+
{insights}
|
116 |
+
|
117 |
+
Additional context from summaries:
|
118 |
+
{summaries}
|
119 |
+
|
120 |
+
User's goal: {user_goal}
|
121 |
+
User's persona: {persona}
|
122 |
+
|
123 |
+
Provide exactly 2 actionable recommendations:"""
|
124 |
+
|
125 |
+
return prompt_llm(
|
126 |
+
prompt.format(
|
127 |
+
insights=insights,
|
128 |
+
summaries="\n\n".join(summaries),
|
129 |
+
user_goal=user_goal,
|
130 |
+
persona=persona if persona else "General user",
|
131 |
+
)
|
132 |
+
)
|
133 |
+
|
134 |
+
def suggest_next_query(self, insights, summaries, user_goal, persona=""):
|
135 |
+
"""Generate a suggested next search query based on insights, summaries, and user goal."""
|
136 |
+
prompt = f"""
|
137 |
+
Based on the following insights and summaries about code, and considering the user's goal and persona,
|
138 |
+
suggest ONE specific area of the codebase the user should explore next.
|
139 |
+
|
140 |
+
INSIGHTS:
|
141 |
+
{insights}
|
142 |
+
|
143 |
+
SUMMARIES:
|
144 |
+
{summaries}
|
145 |
+
|
146 |
+
USER'S GOAL:
|
147 |
+
{user_goal}
|
148 |
+
|
149 |
+
USER'S PERSONA:
|
150 |
+
{persona if persona else "General user"}
|
151 |
+
|
152 |
+
Suggest a specific, focused area or component (5-10 words) that would help the user find additional
|
153 |
+
information to achieve their goal. This should guide their next exploration of the repository.
|
154 |
+
|
155 |
+
NEXT EXPLORATION AREA:
|
156 |
+
"""
|
157 |
+
|
158 |
+
return prompt_llm(prompt)
|
159 |
+
|
160 |
+
|
161 |
+
class QuestionGeneratorAgent:
|
162 |
+
def __init__(self):
|
163 |
+
self.client = Together(api_key=your_api_key)
|
164 |
+
|
165 |
+
def generate_questions(self, content, category, source):
|
166 |
+
prompt_template_path = os.path.join(os.path.dirname(__file__), "PromptTemplate.json")
|
167 |
+
with open(prompt_template_path, "r") as f:
|
168 |
+
import json
|
169 |
+
prompt_data = json.load(f)
|
170 |
+
template = prompt_data["prompt_template"]
|
171 |
+
|
172 |
+
prompt = template.format(
|
173 |
+
content=content,
|
174 |
+
category=category,
|
175 |
+
source=source
|
176 |
+
)
|
177 |
+
|
178 |
+
return prompt_llm(prompt)
|
179 |
+
|
180 |
+
|
181 |
+
class CLISetupAgent:
|
182 |
+
def __init__(self):
|
183 |
+
self.client = Together(api_key=your_api_key)
|
184 |
+
|
185 |
+
def generate_setup_instructions(self, repo_content, repo_metadata):
|
186 |
+
"""Generate step-by-step CLI instructions to set up the environment for a repository."""
|
187 |
+
language = repo_metadata.get("language", "")
|
188 |
+
repo_name = repo_metadata.get("name", "")
|
189 |
+
repo_url = repo_metadata.get("url", "")
|
190 |
+
|
191 |
+
# Collect all potential setup files in the repo
|
192 |
+
setup_files = {}
|
193 |
+
common_setup_files = [
|
194 |
+
"requirements.txt", "package.json", "setup.py", "Dockerfile",
|
195 |
+
"docker-compose.yml", ".env.example", "Makefile", "README.md"
|
196 |
+
]
|
197 |
+
|
198 |
+
for filename, content in repo_content.items():
|
199 |
+
if filename in common_setup_files or filename.endswith((".yml", ".yaml", ".sh", ".bat")):
|
200 |
+
setup_files[filename] = content
|
201 |
+
|
202 |
+
# Default setup steps if no specific files are found
|
203 |
+
default_steps = f"""
|
204 |
+
1. Clone the repository:
|
205 |
+
```
|
206 |
+
git clone {repo_url}
|
207 |
+
cd {repo_name}
|
208 |
+
```
|
209 |
+
|
210 |
+
2. Check the repository structure:
|
211 |
+
```
|
212 |
+
ls -la
|
213 |
+
```
|
214 |
+
|
215 |
+
3. Read the README file for specific instructions:
|
216 |
+
```
|
217 |
+
cat README.md
|
218 |
+
```
|
219 |
+
"""
|
220 |
+
|
221 |
+
# If we have no setup files, provide basic instructions
|
222 |
+
if not setup_files:
|
223 |
+
return default_steps
|
224 |
+
|
225 |
+
# Create a prompt with all the relevant information
|
226 |
+
prompt = f"""
|
227 |
+
SYSTEM: You are an expert DevOps engineer who provides clear CLI setup instructions.
|
228 |
+
|
229 |
+
INSTRUCTIONS:
|
230 |
+
β’ Generate step-by-step CLI instructions to set up a development environment for the given repository
|
231 |
+
β’ The repository is named "{repo_name}" and primarily uses {language if language else "unknown language"}
|
232 |
+
β’ Include commands for cloning, installing dependencies, and basic configuration
|
233 |
+
β’ Format your response as a numbered list with clear command-line instructions
|
234 |
+
β’ Include comments explaining what each command does
|
235 |
+
β’ Focus on practical, executable commands that work on both macOS/Linux and Windows where possible
|
236 |
+
β’ If different platforms require different commands, clearly indicate which is for which
|
237 |
+
β’ Mention any prerequisites that need to be installed (like Python, Node.js, Docker, etc.)
|
238 |
+
|
239 |
+
REPOSITORY INFORMATION:
|
240 |
+
Name: {repo_name}
|
241 |
+
Primary Language: {language if language else "Not specified"}
|
242 |
+
URL: {repo_url}
|
243 |
+
|
244 |
+
RELEVANT SETUP FILES:
|
245 |
+
{chr(10).join([f"--- {name} ---{chr(10)}{content[:300]}..." for name, content in setup_files.items()])}
|
246 |
+
|
247 |
+
Provide a step-by-step CLI setup guide with exactly 5-10 commands:
|
248 |
+
"""
|
249 |
+
|
250 |
+
try:
|
251 |
+
result = prompt_llm(prompt)
|
252 |
+
# Check if result is empty or invalid
|
253 |
+
if not result or len(result.strip()) < 10:
|
254 |
+
return default_steps
|
255 |
+
return result
|
256 |
+
except Exception as e:
|
257 |
+
print(f"Error generating CLI setup: {str(e)}")
|
258 |
+
return default_steps
|
259 |
+
|
260 |
+
|
261 |
+
class ChatbotAgent:
|
262 |
+
"""Agent for answering questions about GitHub repositories."""
|
263 |
+
|
264 |
+
def __init__(self):
|
265 |
+
self.client = Together(api_key=your_api_key)
|
266 |
+
|
267 |
+
def answer_question(self, question, repo_content, repo_metadata, summaries=None, insights=None):
|
268 |
+
"""
|
269 |
+
Answer a question about a GitHub repository based on its content and analysis.
|
270 |
+
|
271 |
+
Args:
|
272 |
+
question: The user's question about the repository
|
273 |
+
repo_content: Dictionary of repository files and their content
|
274 |
+
repo_metadata: Repository metadata like name, description, etc.
|
275 |
+
summaries: Optional dictionary of file summaries
|
276 |
+
insights: Optional insights about the repository
|
277 |
+
|
278 |
+
Returns:
|
279 |
+
A string containing the answer to the question
|
280 |
+
"""
|
281 |
+
# Extract key repository information
|
282 |
+
repo_name = repo_metadata.get("name", "Unknown repository")
|
283 |
+
repo_description = repo_metadata.get("description", "No description available")
|
284 |
+
repo_language = repo_metadata.get("language", "Unknown")
|
285 |
+
|
286 |
+
# Create a context from the repository information
|
287 |
+
context = f"Repository: {repo_name}\nDescription: {repo_description}\nLanguage: {repo_language}\n\n"
|
288 |
+
|
289 |
+
# Add insights if available
|
290 |
+
if insights:
|
291 |
+
context += f"Key insights:\n{insights}\n\n"
|
292 |
+
|
293 |
+
# Add summaries if available
|
294 |
+
if summaries:
|
295 |
+
context += "File summaries:\n"
|
296 |
+
for filename, summary in summaries.items():
|
297 |
+
context += f"- {filename}: {summary}\n"
|
298 |
+
context += "\n"
|
299 |
+
|
300 |
+
# Select relevant files for the question to avoid token limit issues
|
301 |
+
relevant_files = self._select_relevant_files(question, repo_content, max_files=5)
|
302 |
+
|
303 |
+
# Add content of relevant files
|
304 |
+
if relevant_files:
|
305 |
+
context += "Relevant files:\n"
|
306 |
+
for filename, content in relevant_files.items():
|
307 |
+
# Truncate long files
|
308 |
+
if len(content) > 1000:
|
309 |
+
context += f"--- {filename} (truncated) ---\n{content[:1000]}...\n\n"
|
310 |
+
else:
|
311 |
+
context += f"--- {filename} ---\n{content}\n\n"
|
312 |
+
|
313 |
+
# Create the prompt for the LLM
|
314 |
+
prompt = f"""SYSTEM: You are a GitHub repository expert assistant. You provide accurate, helpful answers
|
315 |
+
about code repositories based on their content, structure, and analysis. Draw upon the
|
316 |
+
provided context to answer the question. If you don't know the answer, say so honestly.
|
317 |
+
|
318 |
+
CONTEXT INFORMATION:
|
319 |
+
{context}
|
320 |
+
|
321 |
+
USER QUESTION:
|
322 |
+
{question}
|
323 |
+
|
324 |
+
Provide a clear, concise answer to the question based only on the information provided above.
|
325 |
+
Include code snippets or commands when relevant. Be specific and informative.
|
326 |
+
"""
|
327 |
+
|
328 |
+
return prompt_llm(prompt)
|
329 |
+
|
330 |
+
def _select_relevant_files(self, question, repo_content, max_files=5):
|
331 |
+
"""Select files from the repository that are most relevant to the question."""
|
332 |
+
|
333 |
+
# If there are only a few files, return all of them
|
334 |
+
if len(repo_content) <= max_files:
|
335 |
+
return repo_content
|
336 |
+
|
337 |
+
# For more files, select the most relevant ones based on the question
|
338 |
+
relevant_files = {}
|
339 |
+
|
340 |
+
# Create a prompt to identify relevant file types for the question
|
341 |
+
file_selection_prompt = f"""SYSTEM: You are a code repository expert. Given a question about a repository,
|
342 |
+
identify what types of files would be most relevant to answer it.
|
343 |
+
|
344 |
+
QUESTION: {question}
|
345 |
+
|
346 |
+
Based on this question, list ONLY 3-5 file patterns or extensions that would be most relevant
|
347 |
+
for answering it. For example: 'README.md', '.py', 'package.json', 'Dockerfile', etc.
|
348 |
+
Just list the patterns, one per line, without any explanation or additional text.
|
349 |
+
"""
|
350 |
+
|
351 |
+
# Get relevant file patterns
|
352 |
+
try:
|
353 |
+
file_patterns_response = prompt_llm(file_selection_prompt)
|
354 |
+
file_patterns = [pattern.strip().lower() for pattern in file_patterns_response.split('\n') if pattern.strip()]
|
355 |
+
|
356 |
+
# Filter files based on patterns
|
357 |
+
for filename, content in repo_content.items():
|
358 |
+
filename_lower = filename.lower()
|
359 |
+
|
360 |
+
# Check if file matches any of the patterns
|
361 |
+
if any(pattern in filename_lower for pattern in file_patterns):
|
362 |
+
relevant_files[filename] = content
|
363 |
+
|
364 |
+
# Stop when we reach the maximum number of files
|
365 |
+
if len(relevant_files) >= max_files:
|
366 |
+
break
|
367 |
+
|
368 |
+
# If we didn't find enough files with patterns, add important files
|
369 |
+
if len(relevant_files) < max_files:
|
370 |
+
important_files = ['readme.md', 'setup.py', 'requirements.txt', 'package.json', 'dockerfile']
|
371 |
+
|
372 |
+
for filename, content in repo_content.items():
|
373 |
+
if filename.lower() in important_files and filename not in relevant_files:
|
374 |
+
relevant_files[filename] = content
|
375 |
+
|
376 |
+
# Stop when we reach the maximum number of files
|
377 |
+
if len(relevant_files) >= max_files:
|
378 |
+
break
|
379 |
+
|
380 |
+
# If we still don't have enough files, add some random ones
|
381 |
+
remaining_slots = max_files - len(relevant_files)
|
382 |
+
if remaining_slots > 0:
|
383 |
+
for filename, content in repo_content.items():
|
384 |
+
if filename not in relevant_files:
|
385 |
+
relevant_files[filename] = content
|
386 |
+
remaining_slots -= 1
|
387 |
+
|
388 |
+
if remaining_slots <= 0:
|
389 |
+
break
|
390 |
+
|
391 |
+
except Exception as e:
|
392 |
+
print(f"Error selecting relevant files: {str(e)}")
|
393 |
+
# Fallback: just take the first max_files
|
394 |
+
relevant_files = dict(list(repo_content.items())[:max_files])
|
395 |
+
|
396 |
+
return relevant_files
|
397 |
+
|
398 |
+
|
399 |
+
class PRReviewAgent:
|
400 |
+
"""Agent for reviewing GitHub Pull Requests and providing professional code feedback."""
|
401 |
+
|
402 |
+
def __init__(self):
|
403 |
+
self.client = Together(api_key=your_api_key)
|
404 |
+
|
405 |
+
def review_pr(self, pr_details, target_branch_code):
|
406 |
+
"""
|
407 |
+
Review a GitHub pull request and provide professional code suggestions.
|
408 |
+
|
409 |
+
Args:
|
410 |
+
pr_details: Dictionary containing PR files, metadata, and changes
|
411 |
+
target_branch_code: Dictionary of target branch files and their content
|
412 |
+
|
413 |
+
Returns:
|
414 |
+
A dictionary containing code suggestions and optimization recommendations
|
415 |
+
"""
|
416 |
+
# Extract PR information
|
417 |
+
pr_title = pr_details.get("title", "Untitled PR")
|
418 |
+
pr_description = pr_details.get("description", "No description")
|
419 |
+
changed_files = pr_details.get("changed_files", [])
|
420 |
+
|
421 |
+
# Prepare context for the review
|
422 |
+
context = f"Pull Request: {pr_title}\nDescription: {pr_description}\n\n"
|
423 |
+
|
424 |
+
# Add changed files info
|
425 |
+
if changed_files:
|
426 |
+
context += "Files changed in this PR:\n"
|
427 |
+
for file_info in changed_files:
|
428 |
+
filename = file_info.get("filename", "unknown")
|
429 |
+
changes = file_info.get("patch", "No changes available")
|
430 |
+
context += f"--- {filename} ---\n{changes}\n\n"
|
431 |
+
|
432 |
+
# Add target branch context for the files that were changed
|
433 |
+
relevant_target_files = {}
|
434 |
+
for file_info in changed_files:
|
435 |
+
filename = file_info.get("filename", "")
|
436 |
+
if filename in target_branch_code:
|
437 |
+
relevant_target_files[filename] = target_branch_code[filename]
|
438 |
+
|
439 |
+
if relevant_target_files:
|
440 |
+
context += "Relevant files in target branch:\n"
|
441 |
+
for filename, content in relevant_target_files.items():
|
442 |
+
# Truncate long files
|
443 |
+
if len(content) > 1000:
|
444 |
+
truncated_content = content[:1000] + "..."
|
445 |
+
context += f"--- {filename} (truncated) ---\n{truncated_content}\n\n"
|
446 |
+
else:
|
447 |
+
context += f"--- {filename} ---\n{content}\n\n"
|
448 |
+
|
449 |
+
# Generate code review
|
450 |
+
code_review_prompt = f"""SYSTEM: You are a senior software developer reviewing a GitHub Pull Request.
|
451 |
+
Provide professional, constructive feedback on the code changes. Focus on:
|
452 |
+
|
453 |
+
1. Code style and adherence to best practices
|
454 |
+
2. Potential bugs or issues
|
455 |
+
3. Architecture and design considerations
|
456 |
+
4. Performance implications
|
457 |
+
|
458 |
+
CONTEXT INFORMATION:
|
459 |
+
{context}
|
460 |
+
|
461 |
+
Provide your code review in the following format:
|
462 |
+
|
463 |
+
## Overall Assessment
|
464 |
+
[A brief 2-3 sentence assessment of the PR]
|
465 |
+
|
466 |
+
## Code Quality Suggestions
|
467 |
+
- [Specific suggestion 1 with code example if applicable]
|
468 |
+
- [Specific suggestion 2 with code example if applicable]
|
469 |
+
- [Add more if necessary, at least 3 suggestions]
|
470 |
+
|
471 |
+
## Optimization Opportunities
|
472 |
+
- [Specific optimization 1 with code example if applicable]
|
473 |
+
- [Specific optimization 2 with code example if applicable]
|
474 |
+
- [Add more if necessary, at least 2 suggestions]
|
475 |
+
|
476 |
+
Your review should be professional, specific, and actionable. Provide code examples where appropriate.
|
477 |
+
"""
|
478 |
+
|
479 |
+
review_result = prompt_llm(code_review_prompt)
|
480 |
+
|
481 |
+
return {
|
482 |
+
"review": review_result
|
483 |
+
}
|
app.py
ADDED
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify, url_for, send_from_directory
|
2 |
+
import requests
|
3 |
+
from bs4 import BeautifulSoup
|
4 |
+
from agents import SummarizerAgent, InsightAgent, RecommenderAgent, QuestionGeneratorAgent, CLISetupAgent, ChatbotAgent, PRReviewAgent
|
5 |
+
from github_utils import get_repo_content, get_repo_structure, get_repo_metadata, is_github_url, get_pr_details, get_target_branch_code, verify_github_credentials
|
6 |
+
import os
|
7 |
+
|
8 |
+
app = Flask(__name__)
|
9 |
+
summarizer = SummarizerAgent()
|
10 |
+
insight_agent = InsightAgent()
|
11 |
+
recommender_agent = RecommenderAgent()
|
12 |
+
question_generator = QuestionGeneratorAgent()
|
13 |
+
cli_setup_agent = CLISetupAgent()
|
14 |
+
chatbot_agent = ChatbotAgent() # Initialize the new ChatbotAgent
|
15 |
+
pr_review_agent = PRReviewAgent() # Initialize the new PRReviewAgent
|
16 |
+
|
17 |
+
@app.route('/templates/<path:filename>')
|
18 |
+
def serve_template_file(filename):
|
19 |
+
"""Serve static files from the templates directory."""
|
20 |
+
return send_from_directory(os.path.join(app.root_path, 'templates'), filename)
|
21 |
+
|
22 |
+
@app.route("/", methods=["GET", "POST"])
|
23 |
+
def index():
|
24 |
+
repo_data = None
|
25 |
+
insights = None
|
26 |
+
|
27 |
+
if request.method == "POST":
|
28 |
+
repo_url = request.form.get("repo_url", "")
|
29 |
+
if repo_url and is_github_url(repo_url):
|
30 |
+
# Get repository content
|
31 |
+
repo_data = {
|
32 |
+
"url": repo_url,
|
33 |
+
"metadata": get_repo_metadata(repo_url),
|
34 |
+
"structure": get_repo_structure(repo_url),
|
35 |
+
"content": get_repo_content(repo_url)
|
36 |
+
}
|
37 |
+
|
38 |
+
return render_template(
|
39 |
+
"index.html", repo_data=repo_data, insights=insights
|
40 |
+
)
|
41 |
+
|
42 |
+
@app.route("/summarize", methods=["POST"])
|
43 |
+
def summarize():
|
44 |
+
repo_content = request.json.get("content", {})
|
45 |
+
if not repo_content:
|
46 |
+
return jsonify({"error": "No content provided"}), 400
|
47 |
+
|
48 |
+
try:
|
49 |
+
# Generate summaries for each file
|
50 |
+
summaries = {}
|
51 |
+
for filename, content in repo_content.items():
|
52 |
+
if isinstance(content, str) and len(content) > 0:
|
53 |
+
# Limit content to 1000 words to avoid token limits
|
54 |
+
words = content.split()
|
55 |
+
if len(words) > 1000:
|
56 |
+
content_for_summary = " ".join(words[:1000])
|
57 |
+
else:
|
58 |
+
content_for_summary = content
|
59 |
+
|
60 |
+
# Generate summary
|
61 |
+
summary = summarizer.process(content_for_summary)
|
62 |
+
summaries[filename] = summary
|
63 |
+
|
64 |
+
return jsonify({"summaries": summaries})
|
65 |
+
except Exception as e:
|
66 |
+
return jsonify({"error": f"Error generating summaries: {str(e)}"}), 500
|
67 |
+
|
68 |
+
@app.route("/analyze", methods=["POST"])
|
69 |
+
def analyze():
|
70 |
+
summaries = request.json.get("summaries", {})
|
71 |
+
if not summaries:
|
72 |
+
return jsonify({"error": "No summaries provided"}), 400
|
73 |
+
|
74 |
+
try:
|
75 |
+
# Generate insights from all summaries
|
76 |
+
summary_texts = list(summaries.values())
|
77 |
+
insights = insight_agent.process_text(summary_texts)
|
78 |
+
|
79 |
+
return jsonify({"insights": insights})
|
80 |
+
except Exception as e:
|
81 |
+
return jsonify({"error": f"Error generating insights: {str(e)}"}), 500
|
82 |
+
|
83 |
+
@app.route("/recommend", methods=["POST"])
|
84 |
+
def recommend():
|
85 |
+
data = request.json
|
86 |
+
insights = data.get("insights", "")
|
87 |
+
summaries = data.get("summaries", [])
|
88 |
+
user_goal = data.get("goal", "")
|
89 |
+
persona = data.get("persona", "")
|
90 |
+
|
91 |
+
if not insights or not summaries:
|
92 |
+
return jsonify({"error": "Missing required data"}), 400
|
93 |
+
|
94 |
+
try:
|
95 |
+
recommendations = recommender_agent.process(
|
96 |
+
insights, summaries, user_goal, persona
|
97 |
+
)
|
98 |
+
next_query = recommender_agent.suggest_next_query(
|
99 |
+
insights, summaries, user_goal, persona
|
100 |
+
)
|
101 |
+
return jsonify({"recommendations": recommendations, "next_query": next_query})
|
102 |
+
except Exception as e:
|
103 |
+
return jsonify({"error": f"Error generating recommendations: {str(e)}"}), 500
|
104 |
+
|
105 |
+
@app.route("/generate_questions", methods=["POST"])
|
106 |
+
def generate_questions():
|
107 |
+
data = request.json
|
108 |
+
content = data.get("content", "")
|
109 |
+
category = data.get("category", "repository")
|
110 |
+
source = data.get("source", "")
|
111 |
+
|
112 |
+
if not content or not source:
|
113 |
+
return jsonify({"error": "Missing required data"}), 400
|
114 |
+
|
115 |
+
try:
|
116 |
+
questions = question_generator.generate_questions(content, category, source)
|
117 |
+
return jsonify({"questions": questions})
|
118 |
+
except Exception as e:
|
119 |
+
return jsonify({"error": f"Error generating questions: {str(e)}"}), 500
|
120 |
+
|
121 |
+
@app.route("/workflow", methods=["POST"])
|
122 |
+
def workflow():
|
123 |
+
"""Complete workflow from repository URL to recommendations."""
|
124 |
+
repo_url = request.json.get("repo_url", "")
|
125 |
+
user_goal = request.json.get("goal", "Understand the codebase")
|
126 |
+
persona = request.json.get("persona", "Developer")
|
127 |
+
github_auth = request.json.get("github_auth", None)
|
128 |
+
|
129 |
+
if not repo_url or not is_github_url(repo_url):
|
130 |
+
return jsonify({"error": "Valid GitHub repository URL required"}), 400
|
131 |
+
|
132 |
+
try:
|
133 |
+
# Prepare authentication if provided
|
134 |
+
auth = None
|
135 |
+
if github_auth and 'username' in github_auth and 'token' in github_auth:
|
136 |
+
auth = (github_auth['username'], github_auth['token'])
|
137 |
+
|
138 |
+
# Step 1: Get repository content
|
139 |
+
repo_content = get_repo_content(repo_url, auth=auth)
|
140 |
+
if "error" in repo_content:
|
141 |
+
return jsonify({"error": repo_content["error"]}), 500
|
142 |
+
|
143 |
+
# Get repository metadata
|
144 |
+
repo_metadata = get_repo_metadata(repo_url, auth=auth)
|
145 |
+
# Ensure repo_metadata has the URL
|
146 |
+
if "url" not in repo_metadata:
|
147 |
+
repo_metadata["url"] = repo_url
|
148 |
+
|
149 |
+
# Step 2: Generate summaries
|
150 |
+
summaries = {}
|
151 |
+
for filename, content in repo_content.items():
|
152 |
+
words = content.split()
|
153 |
+
if len(words) > 1000:
|
154 |
+
content_for_summary = " ".join(words[:1000])
|
155 |
+
else:
|
156 |
+
content_for_summary = content
|
157 |
+
|
158 |
+
summary = summarizer.process(content_for_summary)
|
159 |
+
summaries[filename] = summary
|
160 |
+
|
161 |
+
# New Step: Generate CLI setup instructions
|
162 |
+
try:
|
163 |
+
cli_setup = cli_setup_agent.generate_setup_instructions(repo_content, repo_metadata)
|
164 |
+
if not cli_setup or len(cli_setup.strip()) < 10:
|
165 |
+
cli_setup = "Sorry, couldn't generate setup instructions for this repository."
|
166 |
+
except Exception as e:
|
167 |
+
print(f"Error in CLI setup generation: {str(e)}")
|
168 |
+
cli_setup = "Error generating setup instructions. Please check the repository and try again."
|
169 |
+
|
170 |
+
# Step 3: Generate insights
|
171 |
+
summary_texts = list(summaries.values())
|
172 |
+
insights = insight_agent.process_text(summary_texts)
|
173 |
+
|
174 |
+
# Step 4: Generate recommendations
|
175 |
+
recommendations = recommender_agent.process(
|
176 |
+
insights, summary_texts, user_goal, persona
|
177 |
+
)
|
178 |
+
|
179 |
+
# Step 5: Suggest next exploration area
|
180 |
+
next_area = recommender_agent.suggest_next_query(
|
181 |
+
insights, summary_texts, user_goal, persona
|
182 |
+
)
|
183 |
+
|
184 |
+
# Step 6: Generate questions
|
185 |
+
repo_name = repo_metadata.get("name", "GitHub Repository")
|
186 |
+
questions = question_generator.generate_questions(
|
187 |
+
repo_name, "repository", repo_url
|
188 |
+
)
|
189 |
+
|
190 |
+
return jsonify({
|
191 |
+
"summaries": summaries,
|
192 |
+
"cli_setup": cli_setup,
|
193 |
+
"insights": insights,
|
194 |
+
"recommendations": recommendations,
|
195 |
+
"next_area": next_area,
|
196 |
+
"questions": questions,
|
197 |
+
"repo_content": repo_content, # Add repository content for the chatbot
|
198 |
+
"repo_metadata": repo_metadata # Add repository metadata for the chatbot
|
199 |
+
})
|
200 |
+
|
201 |
+
except Exception as e:
|
202 |
+
return jsonify({"error": f"Error in workflow: {str(e)}"}), 500
|
203 |
+
|
204 |
+
@app.route("/chat", methods=["POST"])
|
205 |
+
def chat():
|
206 |
+
"""Handle chatbot questions about a repository."""
|
207 |
+
data = request.json
|
208 |
+
question = data.get("question", "")
|
209 |
+
repo_url = data.get("repo_url", "")
|
210 |
+
repo_content = data.get("repo_content", {})
|
211 |
+
repo_metadata = data.get("repo_metadata", {})
|
212 |
+
summaries = data.get("summaries", {})
|
213 |
+
insights = data.get("insights", "")
|
214 |
+
github_auth = data.get("github_auth", None)
|
215 |
+
|
216 |
+
if not question:
|
217 |
+
return jsonify({"error": "No question provided"}), 400
|
218 |
+
|
219 |
+
# Prepare authentication if provided
|
220 |
+
auth = None
|
221 |
+
if github_auth and 'username' in github_auth and 'token' in github_auth:
|
222 |
+
auth = (github_auth['username'], github_auth['token'])
|
223 |
+
|
224 |
+
if not repo_content and repo_url:
|
225 |
+
# If content isn't provided but URL is, fetch the repository content
|
226 |
+
if is_github_url(repo_url):
|
227 |
+
repo_content = get_repo_content(repo_url, auth=auth)
|
228 |
+
repo_metadata = get_repo_metadata(repo_url, auth=auth)
|
229 |
+
# Ensure repo_metadata has the URL
|
230 |
+
if "url" not in repo_metadata:
|
231 |
+
repo_metadata["url"] = repo_url
|
232 |
+
else:
|
233 |
+
return jsonify({"error": "Valid GitHub repository URL required"}), 400
|
234 |
+
|
235 |
+
if not repo_content:
|
236 |
+
return jsonify({"error": "No repository content provided"}), 400
|
237 |
+
|
238 |
+
try:
|
239 |
+
# Use the chatbot agent to answer the question
|
240 |
+
answer = chatbot_agent.answer_question(
|
241 |
+
question=question,
|
242 |
+
repo_content=repo_content,
|
243 |
+
repo_metadata=repo_metadata,
|
244 |
+
summaries=summaries,
|
245 |
+
insights=insights
|
246 |
+
)
|
247 |
+
|
248 |
+
return jsonify({
|
249 |
+
"answer": answer,
|
250 |
+
"question": question
|
251 |
+
})
|
252 |
+
except Exception as e:
|
253 |
+
return jsonify({"error": f"Error answering question: {str(e)}"}), 500
|
254 |
+
|
255 |
+
@app.route("/review_pr", methods=["POST"])
|
256 |
+
def review_pr():
|
257 |
+
"""Review a GitHub Pull Request and provide professional code suggestions."""
|
258 |
+
pr_url = request.json.get("pr_url", "")
|
259 |
+
max_files = request.json.get("max_files", 25) # Default to 25 files
|
260 |
+
file_types = request.json.get("file_types", None) # Default to all code files
|
261 |
+
github_auth = request.json.get("github_auth", None) # GitHub authentication
|
262 |
+
|
263 |
+
if not pr_url:
|
264 |
+
return jsonify({"error": "No PR URL provided"}), 400
|
265 |
+
|
266 |
+
try:
|
267 |
+
# Prepare authentication if provided
|
268 |
+
auth = None
|
269 |
+
if github_auth and 'username' in github_auth and 'token' in github_auth:
|
270 |
+
auth = (github_auth['username'], github_auth['token'])
|
271 |
+
|
272 |
+
# Step 1: Fetch PR details
|
273 |
+
pr_details = get_pr_details(pr_url, max_files=max_files, file_types=file_types, auth=auth)
|
274 |
+
if "error" in pr_details:
|
275 |
+
return jsonify({"error": pr_details["error"]}), 500
|
276 |
+
|
277 |
+
# Step 2: Fetch target branch code
|
278 |
+
target_branch_code = get_target_branch_code(pr_url, max_files=max_files, file_types=file_types, auth=auth)
|
279 |
+
if "error" in target_branch_code:
|
280 |
+
return jsonify({"error": target_branch_code["error"]}), 500
|
281 |
+
|
282 |
+
# Step 3: Generate PR review
|
283 |
+
review_result = pr_review_agent.review_pr(pr_details, target_branch_code)
|
284 |
+
|
285 |
+
# Step 4: Return the results
|
286 |
+
return jsonify({
|
287 |
+
"pr_title": pr_details.get("title", ""),
|
288 |
+
"pr_user": pr_details.get("user", ""),
|
289 |
+
"target_branch": pr_details.get("target_branch", ""),
|
290 |
+
"source_branch": pr_details.get("source_branch", ""),
|
291 |
+
"changed_files_count": len(pr_details.get("changed_files", [])),
|
292 |
+
"total_file_count": pr_details.get("total_file_count", 0),
|
293 |
+
"review": review_result.get("review", "Error generating review"),
|
294 |
+
"analyzed_files": [file["filename"] for file in pr_details.get("changed_files", [])]
|
295 |
+
})
|
296 |
+
|
297 |
+
except Exception as e:
|
298 |
+
return jsonify({"error": f"Error reviewing PR: {str(e)}"}), 500
|
299 |
+
|
300 |
+
@app.route("/verify_github_credentials", methods=["POST"])
|
301 |
+
def verify_credentials():
|
302 |
+
"""Verify GitHub credentials and return status."""
|
303 |
+
data = request.json
|
304 |
+
github_username = data.get("github_username", "")
|
305 |
+
github_token = data.get("github_token", "")
|
306 |
+
|
307 |
+
if not github_username or not github_token:
|
308 |
+
return jsonify({"valid": False, "error": "Missing username or token"}), 400
|
309 |
+
|
310 |
+
# Verify the credentials
|
311 |
+
is_valid = verify_github_credentials(github_username, github_token)
|
312 |
+
|
313 |
+
if is_valid:
|
314 |
+
return jsonify({"valid": True, "message": "Successfully authenticated with GitHub"})
|
315 |
+
else:
|
316 |
+
return jsonify({"valid": False, "error": "Invalid GitHub credentials"}), 401
|
317 |
+
|
318 |
+
if __name__ == "__main__":
|
319 |
+
app.run(debug=True, port=5001, host="0.0.0.0")
|
github_utils.py
ADDED
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from bs4 import BeautifulSoup
|
3 |
+
import base64
|
4 |
+
import re
|
5 |
+
import os
|
6 |
+
from urllib.parse import urlparse
|
7 |
+
|
8 |
+
def is_github_url(url):
|
9 |
+
"""Check if a URL is a GitHub repository URL."""
|
10 |
+
parsed = urlparse(url)
|
11 |
+
return parsed.netloc in ['github.com', 'www.github.com']
|
12 |
+
|
13 |
+
def parse_github_url(url):
|
14 |
+
"""Extract owner and repo from GitHub URL."""
|
15 |
+
parts = url.strip('/').split('/')
|
16 |
+
if 'github.com' in parts:
|
17 |
+
idx = parts.index('github.com')
|
18 |
+
if len(parts) > idx + 2:
|
19 |
+
owner = parts[idx + 1]
|
20 |
+
repo = parts[idx + 2]
|
21 |
+
return owner, repo
|
22 |
+
return None, None
|
23 |
+
|
24 |
+
def get_repo_content(url, auth=None):
|
25 |
+
"""
|
26 |
+
Get content from a GitHub repository using GitHub's API.
|
27 |
+
Returns a dictionary of filenames and their content.
|
28 |
+
|
29 |
+
Args:
|
30 |
+
url: GitHub repository URL
|
31 |
+
auth: Optional tuple of (username, token) for authentication
|
32 |
+
"""
|
33 |
+
owner, repo = parse_github_url(url)
|
34 |
+
if not owner or not repo:
|
35 |
+
return {"error": "Invalid GitHub URL format"}
|
36 |
+
|
37 |
+
try:
|
38 |
+
# Fetch repository contents
|
39 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/contents"
|
40 |
+
headers = {}
|
41 |
+
|
42 |
+
# Add authentication if provided
|
43 |
+
if auth and len(auth) == 2:
|
44 |
+
username, token = auth
|
45 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
46 |
+
headers["Authorization"] = f"Basic {auth_header}"
|
47 |
+
|
48 |
+
response = requests.get(api_url, headers=headers)
|
49 |
+
response.raise_for_status()
|
50 |
+
|
51 |
+
contents = response.json()
|
52 |
+
repo_content = {}
|
53 |
+
|
54 |
+
# Process each file/directory
|
55 |
+
for item in contents:
|
56 |
+
if item['type'] == 'file' and item['name'].endswith(('.py', '.js', '.html', '.css', '.md')):
|
57 |
+
# Get file content
|
58 |
+
file_response = requests.get(item['url'], headers=headers)
|
59 |
+
file_response.raise_for_status()
|
60 |
+
file_data = file_response.json()
|
61 |
+
|
62 |
+
if 'content' in file_data:
|
63 |
+
content = base64.b64decode(file_data['content']).decode('utf-8')
|
64 |
+
repo_content[item['name']] = content
|
65 |
+
|
66 |
+
# Limit to first 5 files to avoid exceeding API limits
|
67 |
+
if len(repo_content) >= 5:
|
68 |
+
break
|
69 |
+
|
70 |
+
return repo_content
|
71 |
+
|
72 |
+
except Exception as e:
|
73 |
+
return {"error": f"Error fetching repository: {str(e)}"}
|
74 |
+
|
75 |
+
def get_repo_structure(url, auth=None):
|
76 |
+
"""
|
77 |
+
Get the structure of a GitHub repository.
|
78 |
+
Returns a list of file paths in the repository.
|
79 |
+
|
80 |
+
Args:
|
81 |
+
url: GitHub repository URL
|
82 |
+
auth: Optional tuple of (username, token) for authentication
|
83 |
+
"""
|
84 |
+
owner, repo = parse_github_url(url)
|
85 |
+
if not owner or not repo:
|
86 |
+
return {"error": "Invalid GitHub URL format"}
|
87 |
+
|
88 |
+
try:
|
89 |
+
# Prepare headers for authentication
|
90 |
+
headers = {}
|
91 |
+
if auth and len(auth) == 2:
|
92 |
+
username, token = auth
|
93 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
94 |
+
headers["Authorization"] = f"Basic {auth_header}"
|
95 |
+
|
96 |
+
# Use GitHub's API to get repository contents
|
97 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/main?recursive=1"
|
98 |
+
response = requests.get(api_url, headers=headers)
|
99 |
+
|
100 |
+
# If 'main' branch doesn't exist, try 'master'
|
101 |
+
if response.status_code != 200:
|
102 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/master?recursive=1"
|
103 |
+
response = requests.get(api_url, headers=headers)
|
104 |
+
|
105 |
+
response.raise_for_status()
|
106 |
+
data = response.json()
|
107 |
+
|
108 |
+
# Extract file paths
|
109 |
+
files = [item['path'] for item in data['tree'] if item['type'] == 'blob']
|
110 |
+
return files
|
111 |
+
|
112 |
+
except Exception as e:
|
113 |
+
return {"error": f"Error fetching repository structure: {str(e)}"}
|
114 |
+
|
115 |
+
def get_repo_metadata(url, auth=None):
|
116 |
+
"""
|
117 |
+
Get metadata about a GitHub repository such as description, stars, etc.
|
118 |
+
|
119 |
+
Args:
|
120 |
+
url: GitHub repository URL
|
121 |
+
auth: Optional tuple of (username, token) for authentication
|
122 |
+
"""
|
123 |
+
owner, repo = parse_github_url(url)
|
124 |
+
if not owner or not repo:
|
125 |
+
return {"error": "Invalid GitHub URL format"}
|
126 |
+
|
127 |
+
try:
|
128 |
+
# Use GitHub's API to get repository information
|
129 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}"
|
130 |
+
|
131 |
+
# Prepare headers for authentication
|
132 |
+
headers = {}
|
133 |
+
if auth and len(auth) == 2:
|
134 |
+
username, token = auth
|
135 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
136 |
+
headers["Authorization"] = f"Basic {auth_header}"
|
137 |
+
|
138 |
+
response = requests.get(api_url, headers=headers)
|
139 |
+
response.raise_for_status()
|
140 |
+
|
141 |
+
data = response.json()
|
142 |
+
return {
|
143 |
+
"name": data.get("name", ""),
|
144 |
+
"description": data.get("description", ""),
|
145 |
+
"stars": data.get("stargazers_count", 0),
|
146 |
+
"forks": data.get("forks_count", 0),
|
147 |
+
"language": data.get("language", ""),
|
148 |
+
"url": data.get("html_url", "")
|
149 |
+
}
|
150 |
+
|
151 |
+
except Exception as e:
|
152 |
+
return {"error": f"Error fetching repository metadata: {str(e)}"}
|
153 |
+
|
154 |
+
def parse_github_pr_url(url):
|
155 |
+
"""Extract owner, repo, and PR number from GitHub PR URL."""
|
156 |
+
pattern = r'https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)'
|
157 |
+
match = re.match(pattern, url)
|
158 |
+
if match:
|
159 |
+
owner, repo, pr_number = match.groups()
|
160 |
+
return owner, repo, pr_number
|
161 |
+
return None, None, None
|
162 |
+
|
163 |
+
def get_pr_details(pr_url, max_files=25, file_types=None, auth=None):
|
164 |
+
"""
|
165 |
+
Get details of a GitHub Pull Request including changed files and their contents.
|
166 |
+
Returns a dictionary with PR metadata and changes.
|
167 |
+
|
168 |
+
Args:
|
169 |
+
pr_url: URL of the GitHub PR
|
170 |
+
max_files: Maximum number of files to fetch (default: 25)
|
171 |
+
file_types: List of file extensions to include (default: None = all code files)
|
172 |
+
auth: Optional tuple of (username, token) for authentication
|
173 |
+
"""
|
174 |
+
owner, repo, pr_number = parse_github_pr_url(pr_url)
|
175 |
+
if not owner or not repo or not pr_number:
|
176 |
+
return {"error": "Invalid GitHub PR URL format"}
|
177 |
+
|
178 |
+
try:
|
179 |
+
# Prepare headers for authentication
|
180 |
+
headers = {}
|
181 |
+
if auth and len(auth) == 2:
|
182 |
+
username, token = auth
|
183 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
184 |
+
headers["Authorization"] = f"Basic {auth_header}"
|
185 |
+
|
186 |
+
# Fetch PR information
|
187 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
|
188 |
+
response = requests.get(api_url, headers=headers)
|
189 |
+
response.raise_for_status()
|
190 |
+
|
191 |
+
pr_data = response.json()
|
192 |
+
|
193 |
+
# Get PR metadata
|
194 |
+
pr_details = {
|
195 |
+
"title": pr_data.get("title", ""),
|
196 |
+
"description": pr_data.get("body", ""),
|
197 |
+
"user": pr_data.get("user", {}).get("login", ""),
|
198 |
+
"state": pr_data.get("state", ""),
|
199 |
+
"created_at": pr_data.get("created_at", ""),
|
200 |
+
"updated_at": pr_data.get("updated_at", ""),
|
201 |
+
"target_branch": pr_data.get("base", {}).get("ref", ""),
|
202 |
+
"source_branch": pr_data.get("head", {}).get("ref", ""),
|
203 |
+
"changed_files": [],
|
204 |
+
"total_file_count": pr_data.get("changed_files", 0)
|
205 |
+
}
|
206 |
+
|
207 |
+
# Default file types to include if not specified
|
208 |
+
if file_types is None:
|
209 |
+
file_types = ['.py', '.js', '.html', '.css', '.md', '.java', '.ts', '.jsx',
|
210 |
+
'.tsx', '.go', '.c', '.cpp', '.h', '.hpp', '.json', '.yml',
|
211 |
+
'.yaml', '.sh', '.txt', '.sql']
|
212 |
+
|
213 |
+
# Fetch PR changed files with pagination
|
214 |
+
page = 1
|
215 |
+
|
216 |
+
while True:
|
217 |
+
files_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/files?per_page=100&page={page}"
|
218 |
+
files_response = requests.get(files_url, headers=headers)
|
219 |
+
files_response.raise_for_status()
|
220 |
+
|
221 |
+
files_data = files_response.json()
|
222 |
+
|
223 |
+
# If no more files, break the loop
|
224 |
+
if not files_data:
|
225 |
+
break
|
226 |
+
|
227 |
+
# Process each file in this page
|
228 |
+
for file_data in files_data:
|
229 |
+
filename = file_data.get("filename", "")
|
230 |
+
|
231 |
+
# Skip binary files and non-code files
|
232 |
+
file_ext = os.path.splitext(filename)[1].lower()
|
233 |
+
if file_types and file_ext not in file_types:
|
234 |
+
continue
|
235 |
+
|
236 |
+
file_info = {
|
237 |
+
"filename": filename,
|
238 |
+
"status": file_data.get("status", ""), # added, modified, removed
|
239 |
+
"additions": file_data.get("additions", 0),
|
240 |
+
"deletions": file_data.get("deletions", 0),
|
241 |
+
"patch": file_data.get("patch", "")
|
242 |
+
}
|
243 |
+
|
244 |
+
# Add file content if it exists in the PR
|
245 |
+
if file_data.get("status") != "removed":
|
246 |
+
try:
|
247 |
+
file_content_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{pr_data['head']['sha']}/{filename}"
|
248 |
+
content_response = requests.get(file_content_url, headers=headers)
|
249 |
+
|
250 |
+
if content_response.status_code == 200:
|
251 |
+
file_info["content"] = content_response.text
|
252 |
+
except Exception as e:
|
253 |
+
file_info["content_error"] = str(e)
|
254 |
+
|
255 |
+
pr_details["changed_files"].append(file_info)
|
256 |
+
|
257 |
+
# Stop when we reach the maximum number of files
|
258 |
+
if len(pr_details["changed_files"]) >= max_files:
|
259 |
+
break
|
260 |
+
|
261 |
+
# If we've reached max files or there are no more pages, break
|
262 |
+
if len(pr_details["changed_files"]) >= max_files or len(files_data) < 100:
|
263 |
+
break
|
264 |
+
|
265 |
+
# Move to next page
|
266 |
+
page += 1
|
267 |
+
|
268 |
+
return pr_details
|
269 |
+
|
270 |
+
except Exception as e:
|
271 |
+
return {"error": f"Error fetching PR details: {str(e)}"}
|
272 |
+
|
273 |
+
def get_target_branch_code(pr_url, max_files=25, file_types=None, auth=None):
|
274 |
+
"""
|
275 |
+
Get the code from the target branch of a PR.
|
276 |
+
Returns a dictionary of filenames and their content from the target branch.
|
277 |
+
|
278 |
+
Args:
|
279 |
+
pr_url: URL of the GitHub PR
|
280 |
+
max_files: Maximum number of files to fetch (default: 25)
|
281 |
+
file_types: List of file extensions to include (default: None = all code files)
|
282 |
+
auth: Optional tuple of (username, token) for authentication
|
283 |
+
"""
|
284 |
+
owner, repo, pr_number = parse_github_pr_url(pr_url)
|
285 |
+
if not owner or not repo or not pr_number:
|
286 |
+
return {"error": "Invalid GitHub PR URL format"}
|
287 |
+
|
288 |
+
try:
|
289 |
+
# Prepare headers for authentication
|
290 |
+
headers = {}
|
291 |
+
if auth and len(auth) == 2:
|
292 |
+
username, token = auth
|
293 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
294 |
+
headers["Authorization"] = f"Basic {auth_header}"
|
295 |
+
|
296 |
+
# First get the PR to find the target branch name
|
297 |
+
api_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
|
298 |
+
response = requests.get(api_url, headers=headers)
|
299 |
+
response.raise_for_status()
|
300 |
+
|
301 |
+
pr_data = response.json()
|
302 |
+
target_branch = pr_data.get("base", {}).get("ref", "main") # Default to main if not found
|
303 |
+
|
304 |
+
# Default file types to include if not specified
|
305 |
+
if file_types is None:
|
306 |
+
file_types = ['.py', '.js', '.html', '.css', '.md', '.java', '.ts', '.jsx',
|
307 |
+
'.tsx', '.go', '.c', '.cpp', '.h', '.hpp', '.json', '.yml',
|
308 |
+
'.yaml', '.sh', '.txt', '.sql']
|
309 |
+
|
310 |
+
# Get files that were changed in the PR with pagination
|
311 |
+
page = 1
|
312 |
+
target_branch_code = {}
|
313 |
+
|
314 |
+
while True:
|
315 |
+
files_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/files?per_page=100&page={page}"
|
316 |
+
files_response = requests.get(files_url, headers=headers)
|
317 |
+
files_response.raise_for_status()
|
318 |
+
|
319 |
+
files_data = files_response.json()
|
320 |
+
|
321 |
+
# If no more files, break the loop
|
322 |
+
if not files_data:
|
323 |
+
break
|
324 |
+
|
325 |
+
# Get the changed filenames from this page
|
326 |
+
for file_data in files_data:
|
327 |
+
filename = file_data.get("filename")
|
328 |
+
|
329 |
+
# Skip if filename is None or non-matching extension
|
330 |
+
if not filename:
|
331 |
+
continue
|
332 |
+
|
333 |
+
file_ext = os.path.splitext(filename)[1].lower()
|
334 |
+
if file_types and file_ext not in file_types:
|
335 |
+
continue
|
336 |
+
|
337 |
+
try:
|
338 |
+
# Get file content from target branch
|
339 |
+
file_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{target_branch}/{filename}"
|
340 |
+
file_response = requests.get(file_url, headers=headers)
|
341 |
+
|
342 |
+
if file_response.status_code == 200:
|
343 |
+
target_branch_code[filename] = file_response.text
|
344 |
+
except Exception as e:
|
345 |
+
print(f"Error fetching {filename} from target branch: {str(e)}")
|
346 |
+
|
347 |
+
# Stop when we reach the maximum number of files
|
348 |
+
if len(target_branch_code) >= max_files:
|
349 |
+
break
|
350 |
+
|
351 |
+
# If we've reached max files or there are no more pages, break
|
352 |
+
if len(target_branch_code) >= max_files or len(files_data) < 100:
|
353 |
+
break
|
354 |
+
|
355 |
+
# Move to next page
|
356 |
+
page += 1
|
357 |
+
|
358 |
+
return target_branch_code
|
359 |
+
|
360 |
+
except Exception as e:
|
361 |
+
return {"error": f"Error fetching target branch code: {str(e)}"}
|
362 |
+
|
363 |
+
def verify_github_credentials(username, token):
|
364 |
+
"""
|
365 |
+
Verify GitHub credentials by making a test API call.
|
366 |
+
Returns True if credentials are valid, False otherwise.
|
367 |
+
|
368 |
+
Args:
|
369 |
+
username: GitHub username
|
370 |
+
token: GitHub personal access token
|
371 |
+
"""
|
372 |
+
try:
|
373 |
+
# Create authentication header
|
374 |
+
auth_header = base64.b64encode(f"{username}:{token}".encode()).decode()
|
375 |
+
headers = {"Authorization": f"Basic {auth_header}"}
|
376 |
+
|
377 |
+
# Make a test API call to get user information
|
378 |
+
response = requests.get("https://api.github.com/user", headers=headers)
|
379 |
+
|
380 |
+
# Return True if the request was successful (status code 200)
|
381 |
+
return response.status_code == 200
|
382 |
+
except Exception as e:
|
383 |
+
print(f"Error verifying GitHub credentials: {str(e)}")
|
384 |
+
return False
|
message.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
Create a flask app with templates/index.html using DuckDuckGo deep research agent. When the user search for something on the search bar. Try to search the GitHub Repo and return max 5 results use duckduckgo_search for this.
|
2 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
flask==2.3.3
|
2 |
+
requests==2.31.0
|
3 |
+
duckduckgo-search==3.0.2
|
templates/cat_style.css
ADDED
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--cat-primary: #FF9D87;
|
3 |
+
--cat-secondary: #FFCBA4;
|
4 |
+
--cat-accent: #8C5E58;
|
5 |
+
--cat-dark: #594545;
|
6 |
+
--cat-light: #FFF5E4;
|
7 |
+
}
|
8 |
+
|
9 |
+
body {
|
10 |
+
padding-top: 2rem;
|
11 |
+
padding-bottom: 2rem;
|
12 |
+
background-color: var(--cat-light);
|
13 |
+
font-family: 'Comic Sans MS', cursive, sans-serif;
|
14 |
+
background-image: url('https://www.transparenttextures.com/patterns/paws.png');
|
15 |
+
}
|
16 |
+
|
17 |
+
.loading {
|
18 |
+
display: none;
|
19 |
+
text-align: center;
|
20 |
+
margin: 20px 0;
|
21 |
+
}
|
22 |
+
|
23 |
+
.card {
|
24 |
+
margin-bottom: 20px;
|
25 |
+
border-radius: 15px;
|
26 |
+
border: 2px solid var(--cat-accent);
|
27 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
28 |
+
background-color: white;
|
29 |
+
}
|
30 |
+
|
31 |
+
.card-body {
|
32 |
+
position: relative;
|
33 |
+
}
|
34 |
+
|
35 |
+
.card-title {
|
36 |
+
color: var(--cat-dark);
|
37 |
+
font-weight: bold;
|
38 |
+
display: inline-block;
|
39 |
+
}
|
40 |
+
|
41 |
+
.card-title::before {
|
42 |
+
content: "π± ";
|
43 |
+
}
|
44 |
+
|
45 |
+
.btn-primary {
|
46 |
+
background-color: var(--cat-primary);
|
47 |
+
border-color: var(--cat-accent);
|
48 |
+
color: var(--cat-dark);
|
49 |
+
font-weight: bold;
|
50 |
+
transition: all 0.3s;
|
51 |
+
}
|
52 |
+
|
53 |
+
.btn-primary:hover {
|
54 |
+
background-color: var(--cat-accent);
|
55 |
+
border-color: var(--cat-dark);
|
56 |
+
transform: scale(1.05);
|
57 |
+
}
|
58 |
+
|
59 |
+
h1 {
|
60 |
+
color: var(--cat-accent);
|
61 |
+
text-align: center;
|
62 |
+
font-weight: bold;
|
63 |
+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
64 |
+
}
|
65 |
+
|
66 |
+
h1::before, h1::after {
|
67 |
+
content: " πΎ ";
|
68 |
+
}
|
69 |
+
|
70 |
+
.form-control, .form-select {
|
71 |
+
border: 2px solid var(--cat-secondary);
|
72 |
+
border-radius: 10px;
|
73 |
+
background-color: var(--cat-light);
|
74 |
+
}
|
75 |
+
|
76 |
+
.form-control:focus, .form-select:focus {
|
77 |
+
border-color: var(--cat-primary);
|
78 |
+
box-shadow: 0 0 0 0.25rem rgba(255, 157, 135, 0.25);
|
79 |
+
}
|
80 |
+
|
81 |
+
.form-label {
|
82 |
+
color: var(--cat-dark);
|
83 |
+
font-weight: bold;
|
84 |
+
}
|
85 |
+
|
86 |
+
.cat-corner {
|
87 |
+
position: absolute;
|
88 |
+
width: 50px;
|
89 |
+
height: 50px;
|
90 |
+
opacity: 0.7;
|
91 |
+
}
|
92 |
+
|
93 |
+
.cat-top-right {
|
94 |
+
top: -25px;
|
95 |
+
right: -15px;
|
96 |
+
transform: rotate(45deg);
|
97 |
+
}
|
98 |
+
|
99 |
+
.spinner-border {
|
100 |
+
color: var(--cat-primary) !important;
|
101 |
+
}
|
102 |
+
|
103 |
+
.paw-list {
|
104 |
+
list-style-type: none;
|
105 |
+
padding-left: 10px;
|
106 |
+
}
|
107 |
+
|
108 |
+
.paw-list li::before {
|
109 |
+
content: "πΎ";
|
110 |
+
margin-right: 10px;
|
111 |
+
}
|
112 |
+
|
113 |
+
#loading p {
|
114 |
+
color: var(--cat-accent);
|
115 |
+
font-weight: bold;
|
116 |
+
margin-top: 15px;
|
117 |
+
}
|
118 |
+
|
119 |
+
#loading img {
|
120 |
+
width: 100px;
|
121 |
+
height: auto;
|
122 |
+
}
|
123 |
+
|
124 |
+
.cat-cursor {
|
125 |
+
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='48' viewport='0 0 100 100' style='fill:black;font-size:24px;'><text y='50%'>π±</text></svg>") 16 0, auto;
|
126 |
+
}
|
127 |
+
|
128 |
+
/* Special styles for results section */
|
129 |
+
#results pre {
|
130 |
+
background-color: var(--cat-light);
|
131 |
+
padding: 15px;
|
132 |
+
border-radius: 10px;
|
133 |
+
border: 1px dashed var(--cat-primary);
|
134 |
+
}
|
135 |
+
|
136 |
+
/* CLI setup instructions styling */
|
137 |
+
.cli-setup pre.cli-instructions {
|
138 |
+
background-color: var(--cat-light);
|
139 |
+
color: var(--cat-dark);
|
140 |
+
padding: 15px;
|
141 |
+
border-radius: 10px;
|
142 |
+
font-family: 'Courier New', monospace;
|
143 |
+
white-space: pre-wrap;
|
144 |
+
border-left: 5px solid var(--cat-primary);
|
145 |
+
overflow-x: auto;
|
146 |
+
}
|
147 |
+
|
148 |
+
.cli-setup pre.cli-instructions::before {
|
149 |
+
content: "π $ ";
|
150 |
+
color: var(--cat-primary);
|
151 |
+
}
|
152 |
+
|
153 |
+
/* Footer styles */
|
154 |
+
footer {
|
155 |
+
color: var(--cat-accent);
|
156 |
+
margin-top: 2rem;
|
157 |
+
}
|
158 |
+
|
159 |
+
footer .fa-heart {
|
160 |
+
color: var(--cat-primary);
|
161 |
+
}
|
162 |
+
|
163 |
+
footer .fa-cat, footer .fa-paw {
|
164 |
+
font-size: 20px;
|
165 |
+
margin-right: 0.5rem;
|
166 |
+
}
|
167 |
+
|
168 |
+
/* Chat interface styling */
|
169 |
+
.chat-messages {
|
170 |
+
max-height: calc(80vh - 260px); /* Adjusted to leave room for input box */
|
171 |
+
overflow-y: auto;
|
172 |
+
padding: 10px;
|
173 |
+
border: 1px solid var(--cat-secondary);
|
174 |
+
border-radius: 10px;
|
175 |
+
background-color: var(--cat-light);
|
176 |
+
flex-grow: 1;
|
177 |
+
}
|
178 |
+
|
179 |
+
#chat-container {
|
180 |
+
display: flex;
|
181 |
+
flex-direction: column;
|
182 |
+
min-height: 250px;
|
183 |
+
height: calc(80vh - 200px); /* Take up 80% of viewport height minus space for headers */
|
184 |
+
max-height: 80vh; /* Maximum height is 80% of viewport height */
|
185 |
+
transition: all 0.3s ease;
|
186 |
+
}
|
187 |
+
|
188 |
+
.chat-message {
|
189 |
+
margin-bottom: 15px;
|
190 |
+
padding: 10px;
|
191 |
+
border-radius: 10px;
|
192 |
+
max-width: 80%;
|
193 |
+
}
|
194 |
+
|
195 |
+
.user-message {
|
196 |
+
background-color: var(--cat-primary);
|
197 |
+
color: white;
|
198 |
+
margin-left: auto;
|
199 |
+
border-bottom-right-radius: 2px;
|
200 |
+
}
|
201 |
+
|
202 |
+
.bot-message {
|
203 |
+
background-color: var(--cat-secondary);
|
204 |
+
color: var(--cat-dark);
|
205 |
+
margin-right: auto;
|
206 |
+
border-bottom-left-radius: 2px;
|
207 |
+
}
|
208 |
+
|
209 |
+
.chat-loading {
|
210 |
+
color: var(--cat-accent);
|
211 |
+
font-style: italic;
|
212 |
+
}
|
213 |
+
|
214 |
+
.chat-input-container {
|
215 |
+
margin-top: 10px;
|
216 |
+
}
|
217 |
+
|
218 |
+
.message-time {
|
219 |
+
font-size: 0.7rem;
|
220 |
+
color: #666;
|
221 |
+
display: block;
|
222 |
+
margin-top: 5px;
|
223 |
+
}
|
224 |
+
|
225 |
+
.chat-message::before {
|
226 |
+
content: "";
|
227 |
+
display: block;
|
228 |
+
font-size: 0.8rem;
|
229 |
+
margin-bottom: 5px;
|
230 |
+
}
|
231 |
+
|
232 |
+
.user-message::before {
|
233 |
+
content: "You";
|
234 |
+
color: white;
|
235 |
+
}
|
236 |
+
|
237 |
+
.bot-message::before {
|
238 |
+
content: "Repo Cat π±";
|
239 |
+
color: var(--cat-dark);
|
240 |
+
}
|
241 |
+
|
242 |
+
/* Empty chat messages placeholder */
|
243 |
+
.chat-messages:empty::before {
|
244 |
+
content: "No messages yet. Ask Repo Cat a question about this repository!";
|
245 |
+
color: #999;
|
246 |
+
font-style: italic;
|
247 |
+
display: block;
|
248 |
+
text-align: center;
|
249 |
+
padding: 20px 0;
|
250 |
+
}
|
templates/index.html
ADDED
@@ -0,0 +1,1158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Purr-fect Code Analyzer</title>
|
7 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
9 |
+
<link rel="stylesheet" href="{{ url_for('serve_template_file', filename='cat_style.css') }}">
|
10 |
+
</head>
|
11 |
+
<body class="cat-cursor">
|
12 |
+
<div class="container">
|
13 |
+
<h1 class="mb-4">Purr-fect Code Analyzer</h1>
|
14 |
+
|
15 |
+
<!-- Navigation tabs -->
|
16 |
+
<ul class="nav nav-tabs mb-4" id="mainTabs" role="tablist">
|
17 |
+
<li class="nav-item" role="presentation">
|
18 |
+
<button class="nav-link active" id="agent-tab" data-bs-toggle="tab" data-bs-target="#agent-panel" type="button" role="tab" aria-controls="agent-panel" aria-selected="true">
|
19 |
+
<i class="fas fa-cat me-2"></i>Repository Agent
|
20 |
+
</button>
|
21 |
+
</li>
|
22 |
+
<li class="nav-item" role="presentation">
|
23 |
+
<button class="nav-link" id="repo-tab" data-bs-toggle="tab" data-bs-target="#repo-panel" type="button" role="tab" aria-controls="repo-panel" aria-selected="false">
|
24 |
+
<i class="fas fa-code-branch me-2"></i>Repository Analysis
|
25 |
+
</button>
|
26 |
+
</li>
|
27 |
+
<li class="nav-item" role="presentation">
|
28 |
+
<button class="nav-link" id="pr-tab" data-bs-toggle="tab" data-bs-target="#pr-panel" type="button" role="tab" aria-controls="pr-panel" aria-selected="false">
|
29 |
+
<i class="fas fa-code-pull-request me-2"></i>PR Review
|
30 |
+
</button>
|
31 |
+
</li>
|
32 |
+
<li class="nav-item ms-auto" role="presentation">
|
33 |
+
<button class="nav-link" id="login-tab" data-bs-toggle="tab" data-bs-target="#login-panel" type="button" role="tab" aria-controls="login-panel" aria-selected="false">
|
34 |
+
<i class="fas fa-sign-in-alt me-2"></i>GitHub Login
|
35 |
+
</button>
|
36 |
+
</li>
|
37 |
+
</ul>
|
38 |
+
|
39 |
+
<!-- Tab content -->
|
40 |
+
<div class="tab-content" id="mainTabsContent">
|
41 |
+
<!-- Repository Agent Tab (New default tab) -->
|
42 |
+
<div class="tab-pane fade show active" id="agent-panel" role="tabpanel" aria-labelledby="agent-tab">
|
43 |
+
<div class="card mb-4">
|
44 |
+
<div class="card-body">
|
45 |
+
<h5 class="card-title">Connect to GitHub Repository</h5>
|
46 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864514.png" class="cat-corner cat-top-right">
|
47 |
+
<form id="agentRepoForm">
|
48 |
+
<div class="mb-3">
|
49 |
+
<label for="agent_repo_url" class="form-label">GitHub Repository URL <i class="fas fa-cat" style="color: var(--cat-accent);"></i></label>
|
50 |
+
<input type="url" class="form-control" id="agent_repo_url" name="agent_repo_url"
|
51 |
+
placeholder="https://github.com/username/repository" required>
|
52 |
+
</div>
|
53 |
+
<button type="submit" class="btn btn-primary">
|
54 |
+
<i class="fas fa-plug me-2"></i> Connect to Repository
|
55 |
+
</button>
|
56 |
+
</form>
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
|
60 |
+
<div id="agent-loading" class="loading">
|
61 |
+
<img src="https://media.giphy.com/media/VbnUQpnihPSIgIXuZv/giphy.gif" alt="Cat loading animation">
|
62 |
+
<p>Connecting to repository... Our cat agent is preparing for communication!</p>
|
63 |
+
</div>
|
64 |
+
|
65 |
+
<div id="agent-interface" class="d-none">
|
66 |
+
<div class="card mb-3">
|
67 |
+
<div class="card-body">
|
68 |
+
<h5 class="card-title">Chat with Repo Cat</h5>
|
69 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864654.png" class="cat-corner cat-top-right">
|
70 |
+
<p class="card-text">Ask any question about this repository and get immediate answers!</p>
|
71 |
+
|
72 |
+
<div id="chat-container" class="d-flex flex-column">
|
73 |
+
<div id="chat-messages" class="chat-messages mb-3 flex-grow-1"></div>
|
74 |
+
|
75 |
+
<div class="chat-input-container d-flex mt-auto">
|
76 |
+
<input type="text" id="chat-input" class="form-control me-2"
|
77 |
+
placeholder="Ask about setup, functionality, or code structure...">
|
78 |
+
<button id="chat-submit" class="btn btn-primary">
|
79 |
+
<i class="fas fa-paper-plane"></i>
|
80 |
+
</button>
|
81 |
+
</div>
|
82 |
+
<div id="chat-loading" class="chat-loading mt-2 d-none">
|
83 |
+
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
84 |
+
<span class="visually-hidden">Loading...</span>
|
85 |
+
</div>
|
86 |
+
<span class="ms-2">Repo Cat is thinking...</span>
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
|
94 |
+
<!-- Repository Analysis Tab (No longer default) -->
|
95 |
+
<div class="tab-pane fade" id="repo-panel" role="tabpanel" aria-labelledby="repo-tab">
|
96 |
+
<div class="card mb-4">
|
97 |
+
<div class="card-body">
|
98 |
+
<h5 class="card-title">Analyze GitHub Repository</h5>
|
99 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864514.png" class="cat-corner cat-top-right">
|
100 |
+
<form id="repoForm">
|
101 |
+
<div class="mb-3">
|
102 |
+
<label for="repo_url" class="form-label">GitHub Repository URL <i class="fas fa-cat" style="color: var(--cat-accent);"></i></label>
|
103 |
+
<input type="url" class="form-control" id="repo_url" name="repo_url"
|
104 |
+
placeholder="https://github.com/username/repository" required>
|
105 |
+
</div>
|
106 |
+
<div class="mb-3">
|
107 |
+
<label for="goal" class="form-label">Your Goal <i class="fas fa-fish" style="color: var(--cat-primary);"></i></label>
|
108 |
+
<input type="text" class="form-control" id="goal" name="goal"
|
109 |
+
placeholder="Understand the codebase" value="Understand the codebase">
|
110 |
+
</div>
|
111 |
+
<div class="mb-3">
|
112 |
+
<label for="persona" class="form-label">Your Purr-sona <i class="fas fa-paw" style="color: var(--cat-accent);"></i></label>
|
113 |
+
<select class="form-select" id="persona" name="persona">
|
114 |
+
<option value="Developer">Developer Cat</option>
|
115 |
+
<option value="Project Manager">Project Manager Cat</option>
|
116 |
+
<option value="Student">Student Kitten</option>
|
117 |
+
<option value="Researcher">Researcher Cat</option>
|
118 |
+
</select>
|
119 |
+
</div>
|
120 |
+
<button type="submit" class="btn btn-primary">
|
121 |
+
<i class="fas fa-cat me-2"></i> Analyze Re-paw-sitory
|
122 |
+
</button>
|
123 |
+
</form>
|
124 |
+
</div>
|
125 |
+
</div>
|
126 |
+
|
127 |
+
<div id="loading" class="loading">
|
128 |
+
<img src="https://media.giphy.com/media/LmNwrBhejkK9EFP504/giphy.gif" alt="Cat typing animation">
|
129 |
+
<p>Analyzing repository... Our cat experts are pawing through the code!</p>
|
130 |
+
</div>
|
131 |
+
|
132 |
+
<div id="results" class="d-none">
|
133 |
+
<div class="card mb-3">
|
134 |
+
<div class="card-body">
|
135 |
+
<h5 class="card-title">Re-paw-sitory Summaries</h5>
|
136 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864612.png" class="cat-corner cat-top-right">
|
137 |
+
<div id="summaries"></div>
|
138 |
+
</div>
|
139 |
+
</div>
|
140 |
+
|
141 |
+
<!-- New card for CLI setup instructions -->
|
142 |
+
<div class="card mb-3">
|
143 |
+
<div class="card-body">
|
144 |
+
<h5 class="card-title">Purr-fect Setup Instructions</h5>
|
145 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864503.png" class="cat-corner cat-top-right">
|
146 |
+
<div id="cliSetup" class="cli-setup"></div>
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<div class="card mb-3">
|
151 |
+
<div class="card-body">
|
152 |
+
<h5 class="card-title">Key In-sights</h5>
|
153 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864493.png" class="cat-corner cat-top-right">
|
154 |
+
<div id="insights"></div>
|
155 |
+
</div>
|
156 |
+
</div>
|
157 |
+
|
158 |
+
<div class="card mb-3">
|
159 |
+
<div class="card-body">
|
160 |
+
<h5 class="card-title">Meow-commendations</h5>
|
161 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864475.png" class="cat-corner cat-top-right">
|
162 |
+
<div id="recommendations"></div>
|
163 |
+
</div>
|
164 |
+
</div>
|
165 |
+
|
166 |
+
<div class="card mb-3">
|
167 |
+
<div class="card-body">
|
168 |
+
<h5 class="card-title">Next Expurr-loration Area</h5>
|
169 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864470.png" class="cat-corner cat-top-right">
|
170 |
+
<div id="nextArea"></div>
|
171 |
+
</div>
|
172 |
+
</div>
|
173 |
+
|
174 |
+
<div class="card mb-3">
|
175 |
+
<div class="card-body">
|
176 |
+
<h5 class="card-title">Quiz Questions (Cat-egories)</h5>
|
177 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864504.png" class="cat-corner cat-top-right">
|
178 |
+
<div id="questions"></div>
|
179 |
+
</div>
|
180 |
+
</div>
|
181 |
+
</div>
|
182 |
+
</div>
|
183 |
+
|
184 |
+
<!-- PR Review Tab -->
|
185 |
+
<div class="tab-pane fade" id="pr-panel" role="tabpanel" aria-labelledby="pr-tab">
|
186 |
+
<div class="card mb-4">
|
187 |
+
<div class="card-body">
|
188 |
+
<h5 class="card-title">Review GitHub Pull Request</h5>
|
189 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864490.png" class="cat-corner cat-top-right">
|
190 |
+
<form id="prForm">
|
191 |
+
<div class="mb-3">
|
192 |
+
<label for="pr_url" class="form-label">GitHub Pull Request URL <i class="fas fa-code-branch" style="color: var(--cat-accent);"></i></label>
|
193 |
+
<input type="url" class="form-control" id="pr_url" name="pr_url"
|
194 |
+
placeholder="https://github.com/username/repository/pull/123" required>
|
195 |
+
<div class="form-text">Enter the URL of a GitHub PR you want to review</div>
|
196 |
+
</div>
|
197 |
+
|
198 |
+
<div class="row mb-3">
|
199 |
+
<div class="col-md-6">
|
200 |
+
<label for="max_files" class="form-label">Maximum Files to Analyze <i class="fas fa-file-code" style="color: var(--cat-primary);"></i></label>
|
201 |
+
<select class="form-select" id="max_files" name="max_files">
|
202 |
+
<option value="10" selected>10 files</option>
|
203 |
+
<option value="25" >25 files</option>
|
204 |
+
<option value="50">50 files</option>
|
205 |
+
<option value="100">100 files</option>
|
206 |
+
</select>
|
207 |
+
<div class="form-text">Large PRs may take longer to analyze</div>
|
208 |
+
</div>
|
209 |
+
<div class="col-md-6">
|
210 |
+
<label for="file_type_filter" class="form-label">Filter by File Type <i class="fas fa-filter" style="color: var(--cat-accent);"></i></label>
|
211 |
+
<select class="form-select" id="file_type_filter" name="file_type_filter">
|
212 |
+
<option value="all" selected>All Code Files</option>
|
213 |
+
<option value="python">Python (.py)</option>
|
214 |
+
<option value="javascript">JavaScript (.js, .jsx, .ts, .tsx)</option>
|
215 |
+
<option value="web">Web (HTML, CSS)</option>
|
216 |
+
<option value="data">Data Files (JSON, YAML, SQL)</option>
|
217 |
+
<option value="docs">Documentation (MD, TXT)</option>
|
218 |
+
</select>
|
219 |
+
</div>
|
220 |
+
</div>
|
221 |
+
|
222 |
+
<button type="submit" class="btn btn-primary">
|
223 |
+
<i class="fas fa-paw me-2"></i> Fur-fect Code Review
|
224 |
+
</button>
|
225 |
+
</form>
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
|
229 |
+
<div id="pr-loading" class="loading">
|
230 |
+
<img src="https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif" alt="Cat reviewing code animation">
|
231 |
+
<p>Reviewing PR... Our senior developer cat is analyzing your code changes!</p>
|
232 |
+
</div>
|
233 |
+
|
234 |
+
<div id="pr-results" class="d-none">
|
235 |
+
<div class="card mb-3">
|
236 |
+
<div class="card-body">
|
237 |
+
<h5 class="card-title">Pull Request Overview</h5>
|
238 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864521.png" class="cat-corner cat-top-right">
|
239 |
+
<div id="pr-overview">
|
240 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
241 |
+
<div>
|
242 |
+
<h6 id="pr-title"></h6>
|
243 |
+
<p class="text-muted mb-0">Created by <span id="pr-user"></span></p>
|
244 |
+
</div>
|
245 |
+
<div>
|
246 |
+
<span class="badge bg-primary rounded-pill" id="pr-files-count"></span>
|
247 |
+
<span class="badge bg-info rounded-pill" id="pr-total-files"></span>
|
248 |
+
</div>
|
249 |
+
</div>
|
250 |
+
<div class="d-flex mt-2">
|
251 |
+
<div class="badge bg-secondary me-2">Target: <span id="pr-target"></span></div>
|
252 |
+
<div class="badge bg-info me-2">Source: <span id="pr-source"></span></div>
|
253 |
+
</div>
|
254 |
+
<div class="alert alert-warning mt-3" id="pr-file-limit-warning">
|
255 |
+
<i class="fas fa-exclamation-triangle me-2"></i>
|
256 |
+
<span id="pr-file-limit-message"></span>
|
257 |
+
</div>
|
258 |
+
</div>
|
259 |
+
</div>
|
260 |
+
</div>
|
261 |
+
|
262 |
+
<div class="card mb-3">
|
263 |
+
<div class="card-body">
|
264 |
+
<h5 class="card-title">Files Analyzed</h5>
|
265 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864464.png" class="cat-corner cat-top-right">
|
266 |
+
<div id="pr-files-analyzed"></div>
|
267 |
+
</div>
|
268 |
+
</div>
|
269 |
+
|
270 |
+
<div class="card mb-3">
|
271 |
+
<div class="card-body">
|
272 |
+
<h5 class="card-title">Senior Developer Review</h5>
|
273 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864464.png" class="cat-corner cat-top-right">
|
274 |
+
<div id="pr-review" class="review-content"></div>
|
275 |
+
</div>
|
276 |
+
</div>
|
277 |
+
</div>
|
278 |
+
</div>
|
279 |
+
|
280 |
+
<!-- GitHub Login Tab -->
|
281 |
+
<div class="tab-pane fade" id="login-panel" role="tabpanel" aria-labelledby="login-tab">
|
282 |
+
<div class="card mb-4">
|
283 |
+
<div class="card-body">
|
284 |
+
<h5 class="card-title">GitHub Authentication</h5>
|
285 |
+
<img src="https://cdn-icons-png.flaticon.com/512/1864/1864509.png" class="cat-corner cat-top-right">
|
286 |
+
|
287 |
+
<div id="login-status-container" class="mb-4 d-none">
|
288 |
+
<div class="alert alert-success" id="login-success">
|
289 |
+
<i class="fas fa-check-circle me-2"></i>
|
290 |
+
<span>You are logged in to GitHub as <strong id="github-username">username</strong></span>
|
291 |
+
</div>
|
292 |
+
<button id="logout-button" class="btn btn-outline-secondary">
|
293 |
+
<i class="fas fa-sign-out-alt me-2"></i> Logout from GitHub
|
294 |
+
</button>
|
295 |
+
</div>
|
296 |
+
|
297 |
+
<form id="github-login-form">
|
298 |
+
<div class="mb-3">
|
299 |
+
<label for="github_username" class="form-label">GitHub Username <i class="fas fa-user" style="color: var(--cat-accent);"></i></label>
|
300 |
+
<input type="text" class="form-control" id="github_username" name="github_username"
|
301 |
+
placeholder="Your GitHub username" required>
|
302 |
+
</div>
|
303 |
+
<div class="mb-3">
|
304 |
+
<label for="github_token" class="form-label">Personal Access Token <i class="fas fa-key" style="color: var(--cat-primary);"></i></label>
|
305 |
+
<input type="password" class="form-control" id="github_token" name="github_token"
|
306 |
+
placeholder="Your GitHub personal access token" required>
|
307 |
+
<div class="form-text">
|
308 |
+
We recommend using a <a href="https://github.com/settings/tokens" target="_blank">personal access token</a> with 'repo' scope.
|
309 |
+
<br>Your credentials are only stored in your browser's local storage and are never sent to our servers.
|
310 |
+
</div>
|
311 |
+
</div>
|
312 |
+
<button type="submit" class="btn btn-primary">
|
313 |
+
<i class="fas fa-sign-in-alt me-2"></i> Connect to GitHub
|
314 |
+
</button>
|
315 |
+
</form>
|
316 |
+
|
317 |
+
<div class="mt-4">
|
318 |
+
<h6 class="mb-3"><i class="fas fa-info-circle me-2" style="color: var(--cat-accent);"></i>How to create a Personal Access Token:</h6>
|
319 |
+
<ol>
|
320 |
+
<li>Go to your GitHub <a href="https://github.com/settings/tokens" target="_blank">Personal Access Tokens</a> page</li>
|
321 |
+
<li>Click "Generate new token" (classic)</li>
|
322 |
+
<li>Give it a name (e.g., "Purr-fect Code Analyzer")</li>
|
323 |
+
<li>Select the <strong>repo</strong> scope to access private repositories</li>
|
324 |
+
<li>Click "Generate token" and copy the token value</li>
|
325 |
+
<li>Paste the token in the field above</li>
|
326 |
+
</ol>
|
327 |
+
<div class="alert alert-warning">
|
328 |
+
<i class="fas fa-exclamation-triangle me-2"></i>
|
329 |
+
For security, tokens are only shown once when created. If you lose it, you'll need to generate a new one.
|
330 |
+
</div>
|
331 |
+
</div>
|
332 |
+
</div>
|
333 |
+
</div>
|
334 |
+
</div>
|
335 |
+
</div>
|
336 |
+
|
337 |
+
<footer class="text-center mt-5">
|
338 |
+
<p>Created with <i class="fas fa-heart"></i> by the Coding Cats Team</p>
|
339 |
+
<div>
|
340 |
+
<i class="fas fa-cat me-2"></i>
|
341 |
+
<i class="fas fa-paw me-2"></i>
|
342 |
+
<i class="fas fa-cat me-2"></i>
|
343 |
+
<i class="fas fa-paw me-2"></i>
|
344 |
+
<i class="fas fa-cat"></i>
|
345 |
+
</div>
|
346 |
+
</footer>
|
347 |
+
</div>
|
348 |
+
|
349 |
+
<!-- Add Bootstrap JS and needed dependencies -->
|
350 |
+
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
|
351 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
|
352 |
+
|
353 |
+
<script>
|
354 |
+
// GitHub Authentication Utilities - defined early to make them available everywhere
|
355 |
+
// Function to get stored GitHub credentials
|
356 |
+
function getGitHubCredentials() {
|
357 |
+
return {
|
358 |
+
username: localStorage.getItem('github_username'),
|
359 |
+
token: localStorage.getItem('github_token')
|
360 |
+
};
|
361 |
+
}
|
362 |
+
|
363 |
+
// Update all API requests to include GitHub credentials if available
|
364 |
+
function addAuthToRequest(requestData) {
|
365 |
+
const credentials = getGitHubCredentials();
|
366 |
+
|
367 |
+
if (credentials.username && credentials.token) {
|
368 |
+
return {
|
369 |
+
...requestData,
|
370 |
+
github_auth: {
|
371 |
+
username: credentials.username,
|
372 |
+
token: credentials.token
|
373 |
+
}
|
374 |
+
};
|
375 |
+
}
|
376 |
+
|
377 |
+
return requestData;
|
378 |
+
}
|
379 |
+
|
380 |
+
// Check if there are stored GitHub credentials
|
381 |
+
function checkStoredCredentials() {
|
382 |
+
const storedUsername = localStorage.getItem('github_username');
|
383 |
+
const storedToken = localStorage.getItem('github_token');
|
384 |
+
|
385 |
+
if (storedUsername && storedToken) {
|
386 |
+
// Display logged-in status
|
387 |
+
document.getElementById('github-username').textContent = storedUsername;
|
388 |
+
document.getElementById('login-status-container').classList.remove('d-none');
|
389 |
+
document.getElementById('github-login-form').classList.add('d-none');
|
390 |
+
|
391 |
+
// Update login tab to show login status
|
392 |
+
const loginTab = document.getElementById('login-tab');
|
393 |
+
loginTab.innerHTML = `<i class="fas fa-user-check me-2"></i>${storedUsername}`;
|
394 |
+
|
395 |
+
return { username: storedUsername, token: storedToken };
|
396 |
+
} else {
|
397 |
+
// Show login form
|
398 |
+
document.getElementById('login-status-container').classList.add('d-none');
|
399 |
+
document.getElementById('github-login-form').classList.remove('d-none');
|
400 |
+
|
401 |
+
// Reset login tab text
|
402 |
+
const loginTab = document.getElementById('login-tab');
|
403 |
+
loginTab.innerHTML = `<i class="fas fa-sign-in-alt me-2"></i>GitHub Login`;
|
404 |
+
|
405 |
+
return null;
|
406 |
+
}
|
407 |
+
}
|
408 |
+
|
409 |
+
// Repository Analysis Form Handler
|
410 |
+
document.getElementById('repoForm').addEventListener('submit', async function(e) {
|
411 |
+
e.preventDefault();
|
412 |
+
|
413 |
+
const repoUrl = document.getElementById('repo_url').value;
|
414 |
+
const goal = document.getElementById('goal').value;
|
415 |
+
const persona = document.getElementById('persona').value;
|
416 |
+
|
417 |
+
console.log("Repository Analysis form submitted with URL:", repoUrl);
|
418 |
+
|
419 |
+
// Show loading
|
420 |
+
document.getElementById('loading').style.display = 'block';
|
421 |
+
document.getElementById('results').classList.add('d-none');
|
422 |
+
|
423 |
+
try {
|
424 |
+
// Get GitHub credentials if available
|
425 |
+
const credentials = getGitHubCredentials();
|
426 |
+
|
427 |
+
// Prepare the request data with GitHub authentication
|
428 |
+
const requestData = {
|
429 |
+
repo_url: repoUrl,
|
430 |
+
goal: goal,
|
431 |
+
persona: persona
|
432 |
+
};
|
433 |
+
|
434 |
+
// Add GitHub auth if available
|
435 |
+
if (credentials.username && credentials.token) {
|
436 |
+
requestData.github_auth = {
|
437 |
+
username: credentials.username,
|
438 |
+
token: credentials.token
|
439 |
+
};
|
440 |
+
console.log("Adding GitHub auth to Repository Analysis request");
|
441 |
+
}
|
442 |
+
|
443 |
+
console.log("Sending request to /workflow API with data:", JSON.stringify(requestData));
|
444 |
+
|
445 |
+
const response = await fetch('/workflow', {
|
446 |
+
method: 'POST',
|
447 |
+
headers: {
|
448 |
+
'Content-Type': 'application/json',
|
449 |
+
},
|
450 |
+
body: JSON.stringify(requestData),
|
451 |
+
});
|
452 |
+
|
453 |
+
console.log("Received response from /workflow API:", response.status);
|
454 |
+
|
455 |
+
const data = await response.json();
|
456 |
+
console.log("Response data:", data);
|
457 |
+
|
458 |
+
if (response.ok) {
|
459 |
+
// Display summaries
|
460 |
+
const summariesDiv = document.getElementById('summaries');
|
461 |
+
summariesDiv.innerHTML = '';
|
462 |
+
|
463 |
+
Object.entries(data.summaries).forEach(([filename, summary]) => {
|
464 |
+
summariesDiv.innerHTML += `
|
465 |
+
<div class="mb-3">
|
466 |
+
<h6><i class="fas fa-file-code me-2" style="color: var(--cat-primary);"></i>${filename}</h6>
|
467 |
+
<p>${summary}</p>
|
468 |
+
</div>
|
469 |
+
`;
|
470 |
+
});
|
471 |
+
|
472 |
+
// Display CLI setup instructions
|
473 |
+
const setupDiv = document.getElementById('cliSetup');
|
474 |
+
if (data.cli_setup && data.cli_setup.trim().length > 0) {
|
475 |
+
setupDiv.innerHTML = `<pre class="cli-instructions">${data.cli_setup}</pre>`;
|
476 |
+
} else {
|
477 |
+
setupDiv.innerHTML = `<p class="text-muted">No setup instructions available for this repository.</p>`;
|
478 |
+
}
|
479 |
+
|
480 |
+
// Display insights
|
481 |
+
document.getElementById('insights').innerHTML = `<ul class="paw-list">${data.insights.split('\n').map(insight =>
|
482 |
+
`<li>${insight}</li>`).join('')}</ul>`;
|
483 |
+
|
484 |
+
// Display recommendations
|
485 |
+
document.getElementById('recommendations').innerHTML = `<ul class="paw-list">${data.recommendations.split('\n').map(rec =>
|
486 |
+
`<li>${rec}</li>`).join('')}</ul>`;
|
487 |
+
|
488 |
+
// Display next area
|
489 |
+
document.getElementById('nextArea').innerHTML = `<p><i class="fas fa-map-marker-alt me-2" style="color: var(--cat-primary);"></i>${data.next_area}</p>`;
|
490 |
+
|
491 |
+
// Display questions
|
492 |
+
document.getElementById('questions').innerHTML = `<pre>${data.questions}</pre>`;
|
493 |
+
|
494 |
+
// Show results
|
495 |
+
document.getElementById('results').classList.remove('d-none');
|
496 |
+
|
497 |
+
// Scroll to results
|
498 |
+
document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
|
499 |
+
|
500 |
+
// Add a cat emoji to the end of each section
|
501 |
+
const randomCatEmojis = ['π±', 'πΈ', 'πΉ', 'π»', 'π½', 'π', 'πΏ', 'πΎ', 'π', 'πΎ'];
|
502 |
+
document.querySelectorAll('#results .card').forEach(card => {
|
503 |
+
const randomEmoji = randomCatEmojis[Math.floor(Math.random() * randomCatEmojis.length)];
|
504 |
+
const cardBody = card.querySelector('.card-body');
|
505 |
+
const emojiSpan = document.createElement('span');
|
506 |
+
emojiSpan.style.position = 'absolute';
|
507 |
+
emojiSpan.style.bottom = '10px';
|
508 |
+
emojiSpan.style.right = '10px';
|
509 |
+
emojiSpan.style.fontSize = '24px';
|
510 |
+
emojiSpan.textContent = randomEmoji;
|
511 |
+
cardBody.appendChild(emojiSpan);
|
512 |
+
});
|
513 |
+
|
514 |
+
// Store repository data for chat functionality
|
515 |
+
if (window.updateRepoData) {
|
516 |
+
window.updateRepoData(data);
|
517 |
+
}
|
518 |
+
} else {
|
519 |
+
alert('Error: ' + (data.error || 'Failed to analyze repository'));
|
520 |
+
}
|
521 |
+
} catch (error) {
|
522 |
+
console.error('Error:', error);
|
523 |
+
alert('An error occurred. Please try again.');
|
524 |
+
} finally {
|
525 |
+
// Hide loading
|
526 |
+
document.getElementById('loading').style.display = 'none';
|
527 |
+
}
|
528 |
+
});
|
529 |
+
|
530 |
+
// Add some cat fun
|
531 |
+
document.addEventListener('DOMContentLoaded', function() {
|
532 |
+
// Create a small cat that follows the mouse
|
533 |
+
const cat = document.createElement('div');
|
534 |
+
cat.innerHTML = 'π';
|
535 |
+
cat.style.position = 'fixed';
|
536 |
+
cat.style.zIndex = '1000';
|
537 |
+
cat.style.fontSize = '24px';
|
538 |
+
cat.style.pointerEvents = 'none';
|
539 |
+
cat.style.transition = 'transform 0.2s ease-out';
|
540 |
+
document.body.appendChild(cat);
|
541 |
+
|
542 |
+
let lastMouseX = 0;
|
543 |
+
let lastMouseY = 0;
|
544 |
+
|
545 |
+
document.addEventListener('mousemove', function(e) {
|
546 |
+
lastMouseX = e.clientX;
|
547 |
+
lastMouseY = e.clientY;
|
548 |
+
|
549 |
+
// Position the cat slightly behind the cursor
|
550 |
+
cat.style.left = (lastMouseX - 30) + 'px';
|
551 |
+
cat.style.top = (lastMouseY - 10) + 'px';
|
552 |
+
|
553 |
+
// Flip the cat if moving left
|
554 |
+
if (e.movementX < 0) {
|
555 |
+
cat.style.transform = 'scaleX(-1)';
|
556 |
+
} else if (e.movementX > 0) {
|
557 |
+
cat.style.transform = 'scaleX(1)';
|
558 |
+
}
|
559 |
+
});
|
560 |
+
|
561 |
+
// Handle the Repository Agent form submission
|
562 |
+
const agentRepoForm = document.getElementById('agentRepoForm');
|
563 |
+
const agentLoading = document.getElementById('agent-loading');
|
564 |
+
const agentInterface = document.getElementById('agent-interface');
|
565 |
+
|
566 |
+
// Update the agentRepoForm to include GitHub auth
|
567 |
+
agentRepoForm.addEventListener('submit', async function(e) {
|
568 |
+
e.preventDefault();
|
569 |
+
|
570 |
+
const repoUrl = document.getElementById('agent_repo_url').value;
|
571 |
+
|
572 |
+
if (!repoUrl) {
|
573 |
+
alert('Please enter a valid GitHub repository URL');
|
574 |
+
return;
|
575 |
+
}
|
576 |
+
|
577 |
+
// Show loading
|
578 |
+
agentLoading.style.display = 'block';
|
579 |
+
agentInterface.classList.add('d-none');
|
580 |
+
|
581 |
+
try {
|
582 |
+
// Get GitHub credentials if available
|
583 |
+
const requestData = {
|
584 |
+
repo_url: repoUrl,
|
585 |
+
goal: "Chat with repository agent",
|
586 |
+
persona: "Developer"
|
587 |
+
};
|
588 |
+
|
589 |
+
// Add GitHub auth if available
|
590 |
+
const authData = addAuthToRequest(requestData);
|
591 |
+
|
592 |
+
// Fetch repository data
|
593 |
+
const response = await fetch('/workflow', {
|
594 |
+
method: 'POST',
|
595 |
+
headers: {
|
596 |
+
'Content-Type': 'application/json',
|
597 |
+
},
|
598 |
+
body: JSON.stringify(authData),
|
599 |
+
});
|
600 |
+
|
601 |
+
const data = await response.json();
|
602 |
+
|
603 |
+
if (response.ok) {
|
604 |
+
// Store repository data for chat functionality
|
605 |
+
repoData = {
|
606 |
+
url: repoUrl,
|
607 |
+
content: data.repo_content || {},
|
608 |
+
metadata: data.repo_metadata || {},
|
609 |
+
summaries: data.summaries || {},
|
610 |
+
insights: data.insights || ''
|
611 |
+
};
|
612 |
+
|
613 |
+
// Show the chat interface
|
614 |
+
agentInterface.classList.remove('d-none');
|
615 |
+
|
616 |
+
// Scroll to chat interface
|
617 |
+
agentInterface.scrollIntoView({ behavior: 'smooth' });
|
618 |
+
|
619 |
+
// Clear any previous messages
|
620 |
+
if (chatMessages) {
|
621 |
+
chatMessages.innerHTML = '';
|
622 |
+
}
|
623 |
+
|
624 |
+
// Add a welcome message
|
625 |
+
setTimeout(() => {
|
626 |
+
addMessage(`πΊ Meow! I'm connected to ${data.repo_metadata.name || 'your repository'}. Ask me anything about the code structure, setup, or functionality!`, false);
|
627 |
+
}, 500);
|
628 |
+
|
629 |
+
// Add a cat emoji to the chat card
|
630 |
+
const randomCatEmojis = ['π±', 'πΈ', 'πΉ', 'π»', 'π½', 'π', 'πΏ', 'πΎ', 'π', 'πΎ'];
|
631 |
+
const randomEmoji = randomCatEmojis[Math.floor(Math.random() * randomCatEmojis.length)];
|
632 |
+
const cardBody = agentInterface.querySelector('.card-body');
|
633 |
+
const emojiSpan = document.createElement('span');
|
634 |
+
emojiSpan.style.position = 'absolute';
|
635 |
+
emojiSpan.style.bottom = '10px';
|
636 |
+
emojiSpan.style.right = '10px';
|
637 |
+
emojiSpan.style.fontSize = '24px';
|
638 |
+
emojiSpan.textContent = randomEmoji;
|
639 |
+
cardBody.appendChild(emojiSpan);
|
640 |
+
|
641 |
+
// Resize the chat container
|
642 |
+
resizeChatContainer();
|
643 |
+
} else {
|
644 |
+
alert('Error: ' + (data.error || 'Failed to connect to repository'));
|
645 |
+
}
|
646 |
+
} catch (error) {
|
647 |
+
console.error('Error:', error);
|
648 |
+
alert('An error occurred. Please try again.');
|
649 |
+
} finally {
|
650 |
+
// Hide loading
|
651 |
+
agentLoading.style.display = 'none';
|
652 |
+
}
|
653 |
+
});
|
654 |
+
|
655 |
+
// Chat functionality
|
656 |
+
const chatInput = document.getElementById('chat-input');
|
657 |
+
const chatSubmit = document.getElementById('chat-submit');
|
658 |
+
const chatMessages = document.getElementById('chat-messages');
|
659 |
+
const chatLoading = document.getElementById('chat-loading');
|
660 |
+
|
661 |
+
// Initialize repository data storage
|
662 |
+
let repoData = {
|
663 |
+
url: '',
|
664 |
+
content: {},
|
665 |
+
metadata: {},
|
666 |
+
summaries: {},
|
667 |
+
insights: ''
|
668 |
+
};
|
669 |
+
|
670 |
+
// Store the data from workflow response for use in chat
|
671 |
+
document.getElementById('repoForm').addEventListener('submit', function() {
|
672 |
+
// This will be populated when the workflow response comes back
|
673 |
+
setTimeout(() => {
|
674 |
+
const repoUrl = document.getElementById('repo_url').value;
|
675 |
+
repoData.url = repoUrl;
|
676 |
+
}, 100);
|
677 |
+
});
|
678 |
+
|
679 |
+
// Send a message when the send button is clicked
|
680 |
+
chatSubmit.addEventListener('click', sendMessage);
|
681 |
+
|
682 |
+
// Also send a message when Enter is pressed in the input field
|
683 |
+
chatInput.addEventListener('keypress', function(e) {
|
684 |
+
if (e.key === 'Enter') {
|
685 |
+
sendMessage();
|
686 |
+
}
|
687 |
+
});
|
688 |
+
|
689 |
+
function formatTimestamp() {
|
690 |
+
const now = new Date();
|
691 |
+
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
692 |
+
}
|
693 |
+
|
694 |
+
function addMessage(text, isUser = false) {
|
695 |
+
const messageElement = document.createElement('div');
|
696 |
+
messageElement.classList.add('chat-message');
|
697 |
+
messageElement.classList.add(isUser ? 'user-message' : 'bot-message');
|
698 |
+
|
699 |
+
// Format message text to handle code blocks and line breaks
|
700 |
+
let formattedText = text;
|
701 |
+
|
702 |
+
// Convert markdown code blocks to HTML
|
703 |
+
formattedText = formattedText.replace(/```([^`]+)```/g, '<pre><code>$1</code></pre>');
|
704 |
+
|
705 |
+
// Convert inline code to HTML
|
706 |
+
formattedText = formattedText.replace(/`([^`]+)`/g, '<code>$1</code>');
|
707 |
+
|
708 |
+
// Convert line breaks to <br>
|
709 |
+
formattedText = formattedText.replace(/\n/g, '<br>');
|
710 |
+
|
711 |
+
messageElement.innerHTML = `
|
712 |
+
${formattedText}
|
713 |
+
<span class="message-time">${formatTimestamp()}</span>
|
714 |
+
`;
|
715 |
+
|
716 |
+
chatMessages.appendChild(messageElement);
|
717 |
+
|
718 |
+
// Scroll to the bottom of the chat
|
719 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
720 |
+
}
|
721 |
+
|
722 |
+
async function sendMessage() {
|
723 |
+
const message = chatInput.value.trim();
|
724 |
+
if (!message) return;
|
725 |
+
|
726 |
+
// Add user message to chat
|
727 |
+
addMessage(message, true);
|
728 |
+
|
729 |
+
// Clear input field
|
730 |
+
chatInput.value = '';
|
731 |
+
|
732 |
+
// Show loading indicator
|
733 |
+
chatLoading.classList.remove('d-none');
|
734 |
+
|
735 |
+
// Collect data needed for the chat request
|
736 |
+
const repoUrl = repoData.url || document.getElementById('repo_url').value;
|
737 |
+
|
738 |
+
// Collect summaries from displayed content
|
739 |
+
const summariesDiv = document.getElementById('summaries');
|
740 |
+
const summaries = {};
|
741 |
+
if (summariesDiv) {
|
742 |
+
const summaryElements = summariesDiv.querySelectorAll('h6');
|
743 |
+
summaryElements.forEach(el => {
|
744 |
+
const filename = el.innerText;
|
745 |
+
const summary = el.nextElementSibling.innerText;
|
746 |
+
summaries[filename] = summary;
|
747 |
+
});
|
748 |
+
}
|
749 |
+
|
750 |
+
// Get insights
|
751 |
+
const insightsEl = document.getElementById('insights');
|
752 |
+
let insights = '';
|
753 |
+
if (insightsEl) {
|
754 |
+
insights = insightsEl.innerText;
|
755 |
+
}
|
756 |
+
|
757 |
+
try {
|
758 |
+
const response = await fetch('/chat', {
|
759 |
+
method: 'POST',
|
760 |
+
headers: {
|
761 |
+
'Content-Type': 'application/json',
|
762 |
+
},
|
763 |
+
body: JSON.stringify({
|
764 |
+
question: message,
|
765 |
+
repo_url: repoUrl,
|
766 |
+
repo_content: repoData.content,
|
767 |
+
repo_metadata: repoData.metadata,
|
768 |
+
summaries: summaries,
|
769 |
+
insights: insights
|
770 |
+
}),
|
771 |
+
});
|
772 |
+
|
773 |
+
const data = await response.json();
|
774 |
+
|
775 |
+
if (response.ok) {
|
776 |
+
// Add bot response to chat
|
777 |
+
addMessage(data.answer, false);
|
778 |
+
} else {
|
779 |
+
// Add error message
|
780 |
+
addMessage('Sorry, I encountered an error: ' + (data.error || 'Unknown error'), false);
|
781 |
+
}
|
782 |
+
} catch (error) {
|
783 |
+
console.error('Error:', error);
|
784 |
+
addMessage('Sorry, I encountered a technical problem. Please try again.', false);
|
785 |
+
} finally {
|
786 |
+
// Hide loading indicator
|
787 |
+
chatLoading.classList.add('d-none');
|
788 |
+
}
|
789 |
+
}
|
790 |
+
|
791 |
+
// Update stored repo data when workflow completes
|
792 |
+
window.updateRepoData = function(data) {
|
793 |
+
repoData = {
|
794 |
+
url: document.getElementById('repo_url').value,
|
795 |
+
content: data.repo_content || {},
|
796 |
+
metadata: data.repo_metadata || {},
|
797 |
+
summaries: data.summaries || {},
|
798 |
+
insights: data.insights || ''
|
799 |
+
};
|
800 |
+
|
801 |
+
// Add a welcome message from Repo Cat after analysis
|
802 |
+
if (chatMessages && chatMessages.childElementCount === 0) {
|
803 |
+
setTimeout(() => {
|
804 |
+
addMessage('πΊ Meow! I\'ve analyzed this repository and I\'m ready to answer your questions about it. Ask me anything about the code structure, setup, or functionality!', false);
|
805 |
+
}, 1000);
|
806 |
+
}
|
807 |
+
};
|
808 |
+
|
809 |
+
// Add chat container resize functionality
|
810 |
+
function resizeChatContainer() {
|
811 |
+
const chatContainer = document.getElementById('chat-container');
|
812 |
+
const chatMessages = document.getElementById('chat-messages');
|
813 |
+
|
814 |
+
if (!chatContainer || !chatMessages) return;
|
815 |
+
|
816 |
+
// Get viewport height and calculate appropriate height
|
817 |
+
const viewportHeight = window.innerHeight;
|
818 |
+
|
819 |
+
// Default to taking up 80% of viewport height minus space for headers
|
820 |
+
const headerHeight = document.querySelector('h1').offsetHeight +
|
821 |
+
document.querySelector('.nav-tabs').offsetHeight +
|
822 |
+
document.querySelector('#agent-panel .card').offsetHeight + 60; // Add padding
|
823 |
+
|
824 |
+
// Calculate the available height for chat
|
825 |
+
const availableHeight = viewportHeight - headerHeight;
|
826 |
+
const optimalHeight = Math.max(250, Math.min(availableHeight * 0.9, viewportHeight * 0.8));
|
827 |
+
|
828 |
+
// Apply height to chat container
|
829 |
+
chatContainer.style.height = `${optimalHeight}px`;
|
830 |
+
|
831 |
+
// Messages container should have some space for the input box
|
832 |
+
chatMessages.style.maxHeight = `${optimalHeight - 60}px`;
|
833 |
+
}
|
834 |
+
|
835 |
+
// Call resize function when messages are added
|
836 |
+
const originalAddMessage = addMessage;
|
837 |
+
addMessage = function(text, isUser = false) {
|
838 |
+
originalAddMessage(text, isUser);
|
839 |
+
resizeChatContainer();
|
840 |
+
};
|
841 |
+
|
842 |
+
// Call resize on window resize
|
843 |
+
window.addEventListener('resize', resizeChatContainer);
|
844 |
+
|
845 |
+
// Initial resize
|
846 |
+
setTimeout(resizeChatContainer, 100);
|
847 |
+
|
848 |
+
// GitHub Login Form Handling
|
849 |
+
const githubLoginForm = document.getElementById('github-login-form');
|
850 |
+
const loginStatusContainer = document.getElementById('login-status-container');
|
851 |
+
const logoutButton = document.getElementById('logout-button');
|
852 |
+
const githubUsername = document.getElementById('github-username');
|
853 |
+
|
854 |
+
// Check if there are stored GitHub credentials
|
855 |
+
function checkStoredCredentials() {
|
856 |
+
const storedUsername = localStorage.getItem('github_username');
|
857 |
+
const storedToken = localStorage.getItem('github_token');
|
858 |
+
|
859 |
+
if (storedUsername && storedToken) {
|
860 |
+
// Display logged-in status
|
861 |
+
githubUsername.textContent = storedUsername;
|
862 |
+
loginStatusContainer.classList.remove('d-none');
|
863 |
+
githubLoginForm.classList.add('d-none');
|
864 |
+
|
865 |
+
// Update login tab to show login status
|
866 |
+
const loginTab = document.getElementById('login-tab');
|
867 |
+
loginTab.innerHTML = `<i class="fas fa-user-check me-2"></i>${storedUsername}`;
|
868 |
+
|
869 |
+
return { username: storedUsername, token: storedToken };
|
870 |
+
} else {
|
871 |
+
// Show login form
|
872 |
+
loginStatusContainer.classList.add('d-none');
|
873 |
+
githubLoginForm.classList.remove('d-none');
|
874 |
+
|
875 |
+
// Reset login tab text
|
876 |
+
const loginTab = document.getElementById('login-tab');
|
877 |
+
loginTab.innerHTML = `<i class="fas fa-sign-in-alt me-2"></i>GitHub Login`;
|
878 |
+
|
879 |
+
return null;
|
880 |
+
}
|
881 |
+
}
|
882 |
+
|
883 |
+
// Handle GitHub login
|
884 |
+
githubLoginForm.addEventListener('submit', async function(e) {
|
885 |
+
e.preventDefault();
|
886 |
+
|
887 |
+
const username = document.getElementById('github_username').value;
|
888 |
+
const token = document.getElementById('github_token').value;
|
889 |
+
|
890 |
+
if (!username || !token) {
|
891 |
+
alert('Please enter both username and token');
|
892 |
+
return;
|
893 |
+
}
|
894 |
+
|
895 |
+
try {
|
896 |
+
// Verify the credentials by making a test API call
|
897 |
+
const response = await fetch('/verify_github_credentials', {
|
898 |
+
method: 'POST',
|
899 |
+
headers: {
|
900 |
+
'Content-Type': 'application/json',
|
901 |
+
},
|
902 |
+
body: JSON.stringify({
|
903 |
+
github_username: username,
|
904 |
+
github_token: token
|
905 |
+
}),
|
906 |
+
});
|
907 |
+
|
908 |
+
const data = await response.json();
|
909 |
+
|
910 |
+
if (response.ok && data.valid) {
|
911 |
+
// Store credentials in browser's localStorage
|
912 |
+
localStorage.setItem('github_username', username);
|
913 |
+
localStorage.setItem('github_token', token);
|
914 |
+
|
915 |
+
// Update UI to show logged-in status
|
916 |
+
githubUsername.textContent = username;
|
917 |
+
loginStatusContainer.classList.remove('d-none');
|
918 |
+
githubLoginForm.classList.add('d-none');
|
919 |
+
|
920 |
+
// Update login tab to show login status
|
921 |
+
const loginTab = document.getElementById('login-tab');
|
922 |
+
loginTab.innerHTML = `<i class="fas fa-user-check me-2"></i>${username}`;
|
923 |
+
|
924 |
+
alert('Successfully connected to GitHub!');
|
925 |
+
} else {
|
926 |
+
alert('Invalid GitHub credentials. Please check your username and token.');
|
927 |
+
}
|
928 |
+
} catch (error) {
|
929 |
+
console.error('Error:', error);
|
930 |
+
alert('Error verifying GitHub credentials. Please try again.');
|
931 |
+
}
|
932 |
+
});
|
933 |
+
|
934 |
+
// Handle logout
|
935 |
+
logoutButton.addEventListener('click', function() {
|
936 |
+
// Clear stored credentials
|
937 |
+
localStorage.removeItem('github_username');
|
938 |
+
localStorage.removeItem('github_token');
|
939 |
+
|
940 |
+
// Update UI
|
941 |
+
loginStatusContainer.classList.add('d-none');
|
942 |
+
githubLoginForm.classList.remove('d-none');
|
943 |
+
|
944 |
+
// Reset form fields
|
945 |
+
document.getElementById('github_username').value = '';
|
946 |
+
document.getElementById('github_token').value = '';
|
947 |
+
|
948 |
+
// Reset login tab text
|
949 |
+
const loginTab = document.getElementById('login-tab');
|
950 |
+
loginTab.innerHTML = `<i class="fas fa-sign-in-alt me-2"></i>GitHub Login`;
|
951 |
+
|
952 |
+
alert('You have been logged out from GitHub.');
|
953 |
+
});
|
954 |
+
|
955 |
+
// Check for stored credentials on page load
|
956 |
+
document.addEventListener('DOMContentLoaded', function() {
|
957 |
+
checkStoredCredentials();
|
958 |
+
});
|
959 |
+
|
960 |
+
// Function to get stored GitHub credentials
|
961 |
+
function getGitHubCredentials() {
|
962 |
+
return {
|
963 |
+
username: localStorage.getItem('github_username'),
|
964 |
+
token: localStorage.getItem('github_token')
|
965 |
+
};
|
966 |
+
}
|
967 |
+
|
968 |
+
// Update all API requests to include GitHub credentials if available
|
969 |
+
function addAuthToRequest(requestData) {
|
970 |
+
const credentials = getGitHubCredentials();
|
971 |
+
|
972 |
+
if (credentials.username && credentials.token) {
|
973 |
+
return {
|
974 |
+
...requestData,
|
975 |
+
github_auth: {
|
976 |
+
username: credentials.username,
|
977 |
+
token: credentials.token
|
978 |
+
}
|
979 |
+
};
|
980 |
+
}
|
981 |
+
|
982 |
+
return requestData;
|
983 |
+
}
|
984 |
+
|
985 |
+
// PR Review Form Handling
|
986 |
+
const prForm = document.getElementById('prForm');
|
987 |
+
const prLoading = document.getElementById('pr-loading');
|
988 |
+
const prResults = document.getElementById('pr-results');
|
989 |
+
|
990 |
+
prForm.addEventListener('submit', async function(e) {
|
991 |
+
e.preventDefault();
|
992 |
+
|
993 |
+
const prUrl = document.getElementById('pr_url').value;
|
994 |
+
const maxFiles = parseInt(document.getElementById('max_files').value);
|
995 |
+
const fileTypeFilter = document.getElementById('file_type_filter').value;
|
996 |
+
|
997 |
+
if (!prUrl) {
|
998 |
+
alert('Please enter a valid GitHub PR URL');
|
999 |
+
return;
|
1000 |
+
}
|
1001 |
+
|
1002 |
+
// Map the file type filter selection to actual file extensions
|
1003 |
+
let fileTypes = null;
|
1004 |
+
switch(fileTypeFilter) {
|
1005 |
+
case 'python':
|
1006 |
+
fileTypes = ['.py'];
|
1007 |
+
break;
|
1008 |
+
case 'javascript':
|
1009 |
+
fileTypes = ['.js', '.jsx', '.ts', '.tsx'];
|
1010 |
+
break;
|
1011 |
+
case 'web':
|
1012 |
+
fileTypes = ['.html', '.css'];
|
1013 |
+
break;
|
1014 |
+
case 'data':
|
1015 |
+
fileTypes = ['.json', '.yml', '.yaml', '.sql'];
|
1016 |
+
break;
|
1017 |
+
case 'docs':
|
1018 |
+
fileTypes = ['.md', '.txt'];
|
1019 |
+
break;
|
1020 |
+
// 'all' or default: null = all code files
|
1021 |
+
}
|
1022 |
+
|
1023 |
+
// Show loading
|
1024 |
+
prLoading.style.display = 'block';
|
1025 |
+
prResults.classList.add('d-none');
|
1026 |
+
|
1027 |
+
try {
|
1028 |
+
// Scroll to the loading indicator
|
1029 |
+
prLoading.scrollIntoView({ behavior: 'smooth' });
|
1030 |
+
|
1031 |
+
// Prepare the request data
|
1032 |
+
const requestData = {
|
1033 |
+
pr_url: prUrl,
|
1034 |
+
max_files: maxFiles,
|
1035 |
+
file_types: fileTypes
|
1036 |
+
};
|
1037 |
+
|
1038 |
+
// Add GitHub auth if available
|
1039 |
+
const authData = addAuthToRequest(requestData);
|
1040 |
+
|
1041 |
+
const response = await fetch('/review_pr', {
|
1042 |
+
method: 'POST',
|
1043 |
+
headers: {
|
1044 |
+
'Content-Type': 'application/json',
|
1045 |
+
},
|
1046 |
+
body: JSON.stringify(authData),
|
1047 |
+
});
|
1048 |
+
|
1049 |
+
const data = await response.json();
|
1050 |
+
|
1051 |
+
if (response.ok) {
|
1052 |
+
// Display PR details
|
1053 |
+
document.getElementById('pr-title').textContent = data.pr_title;
|
1054 |
+
document.getElementById('pr-user').textContent = data.pr_user;
|
1055 |
+
document.getElementById('pr-target').textContent = data.target_branch;
|
1056 |
+
document.getElementById('pr-source').textContent = data.source_branch;
|
1057 |
+
document.getElementById('pr-files-count').textContent = data.changed_files_count + ' files analyzed';
|
1058 |
+
document.getElementById('pr-total-files').textContent = data.total_file_count + ' total files in PR';
|
1059 |
+
|
1060 |
+
// Show or hide the file limit warning
|
1061 |
+
const fileWarning = document.getElementById('pr-file-limit-warning');
|
1062 |
+
if (data.total_file_count > data.changed_files_count) {
|
1063 |
+
fileWarning.style.display = 'block';
|
1064 |
+
fileWarning.querySelector('#pr-file-limit-message').textContent =
|
1065 |
+
`This PR contains ${data.total_file_count} files, but only ${data.changed_files_count} were analyzed due to size limits. ` +
|
1066 |
+
`Try filtering by file type or increase the maximum files limit to get more comprehensive feedback.`;
|
1067 |
+
} else {
|
1068 |
+
fileWarning.style.display = 'none';
|
1069 |
+
}
|
1070 |
+
|
1071 |
+
// Display the list of analyzed files
|
1072 |
+
const filesAnalyzedDiv = document.getElementById('pr-files-analyzed');
|
1073 |
+
if (data.analyzed_files && data.analyzed_files.length > 0) {
|
1074 |
+
// Group files by directory for better organization
|
1075 |
+
const filesByDir = {};
|
1076 |
+
data.analyzed_files.forEach(file => {
|
1077 |
+
const lastSlash = file.lastIndexOf('/');
|
1078 |
+
const dir = lastSlash > 0 ? file.substring(0, lastSlash) : '/';
|
1079 |
+
if (!filesByDir[dir]) {
|
1080 |
+
filesByDir[dir] = [];
|
1081 |
+
}
|
1082 |
+
filesByDir[dir].push(file);
|
1083 |
+
});
|
1084 |
+
|
1085 |
+
let filesHtml = '';
|
1086 |
+
Object.entries(filesByDir).forEach(([dir, files]) => {
|
1087 |
+
filesHtml += `<div class="mb-2"><strong>${dir || 'Root'}</strong>: `;
|
1088 |
+
filesHtml += files.map(file => {
|
1089 |
+
const fileName = file.substring(file.lastIndexOf('/') + 1);
|
1090 |
+
return `<span class="badge bg-light text-dark me-1">${fileName}</span>`;
|
1091 |
+
}).join('');
|
1092 |
+
filesHtml += '</div>';
|
1093 |
+
});
|
1094 |
+
|
1095 |
+
filesAnalyzedDiv.innerHTML = filesHtml;
|
1096 |
+
} else {
|
1097 |
+
filesAnalyzedDiv.innerHTML = '<p class="text-muted">No files were analyzed. Try changing the file type filter.</p>';
|
1098 |
+
}
|
1099 |
+
|
1100 |
+
// Display review
|
1101 |
+
const reviewContent = document.getElementById('pr-review');
|
1102 |
+
|
1103 |
+
// Format the review content with markdown-like syntax
|
1104 |
+
let formattedReview = data.review;
|
1105 |
+
|
1106 |
+
// Convert markdown headers to HTML
|
1107 |
+
formattedReview = formattedReview.replace(/## ([^\n]+)/g, '<h4>$1</h4>');
|
1108 |
+
|
1109 |
+
// Convert markdown lists to HTML
|
1110 |
+
formattedReview = formattedReview.replace(/- ([^\n]+)/g, '<li>$1</li>');
|
1111 |
+
formattedReview = formattedReview.replace(/<li>/g, '<ul><li>');
|
1112 |
+
formattedReview = formattedReview.replace(/<\/li>\n/g, '</li></ul>\n');
|
1113 |
+
|
1114 |
+
// Convert code blocks
|
1115 |
+
formattedReview = formattedReview.replace(/```([^`]+)```/g, '<pre><code>$1</code></pre>');
|
1116 |
+
|
1117 |
+
// Convert inline code
|
1118 |
+
formattedReview = formattedReview.replace(/`([^`]+)`/g, '<code>$1</code>');
|
1119 |
+
|
1120 |
+
// Handle line breaks
|
1121 |
+
formattedReview = formattedReview.replace(/\n/g, '<br>');
|
1122 |
+
|
1123 |
+
reviewContent.innerHTML = formattedReview;
|
1124 |
+
|
1125 |
+
// Show results
|
1126 |
+
prResults.classList.remove('d-none');
|
1127 |
+
|
1128 |
+
// Scroll to results
|
1129 |
+
prResults.scrollIntoView({ behavior: 'smooth' });
|
1130 |
+
|
1131 |
+
// Add a cat emoji to the end of each section
|
1132 |
+
const randomCatEmojis = ['π±', 'πΈ', 'πΉ', 'π»', 'π½', 'π', 'πΏ', 'πΎ', 'π', 'πΎ'];
|
1133 |
+
document.querySelectorAll('#pr-results .card').forEach(card => {
|
1134 |
+
const randomEmoji = randomCatEmojis[Math.floor(Math.random() * randomCatEmojis.length)];
|
1135 |
+
const cardBody = card.querySelector('.card-body');
|
1136 |
+
const emojiSpan = document.createElement('span');
|
1137 |
+
emojiSpan.style.position = 'absolute';
|
1138 |
+
emojiSpan.style.bottom = '10px';
|
1139 |
+
emojiSpan.style.right = '10px';
|
1140 |
+
emojiSpan.style.fontSize = '24px';
|
1141 |
+
emojiSpan.textContent = randomEmoji;
|
1142 |
+
cardBody.appendChild(emojiSpan);
|
1143 |
+
});
|
1144 |
+
} else {
|
1145 |
+
alert('Error: ' + (data.error || 'Failed to review PR'));
|
1146 |
+
}
|
1147 |
+
} catch (error) {
|
1148 |
+
console.error('Error:', error);
|
1149 |
+
alert('An error occurred. Please try again.');
|
1150 |
+
} finally {
|
1151 |
+
// Hide loading
|
1152 |
+
prLoading.style.display = 'none';
|
1153 |
+
}
|
1154 |
+
});
|
1155 |
+
});
|
1156 |
+
</script>
|
1157 |
+
</body>
|
1158 |
+
</html>
|