From 5e61321376d0332255ecd8ffd192fbbe3701938d Mon Sep 17 00:00:00 2001 From: otdoges Date: Sun, 17 Aug 2025 04:27:25 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Add=20comprehensive=20GitHub=20?= =?UTF-8?q?integration=20to=20chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub repository analysis and PR creation functionality - Users can provide GitHub URLs to fork repos and create PRs with AI-suggested changes - Full GitHub service with authentication, forking, branch management, and PR creation - Beautiful GitHub integration UI with repo selection, file changes, and progress tracking - Smart GitHub context detection and enhancement in chat messages - Visual indicators for GitHub mode with repository information - Seamless workflow: analyze repo → get AI suggestions → apply changes → create PR - Token management with secure localStorage storage and validation - Support for public and private repositories - Real-time operation status with progress indicators - Integration with existing chat interface and AI responses - Scout jam: [0fa5888b-ec59-4025-8e7c-5ae1d6bfb07a](https://scout.new/jam/0fa5888b-ec59-4025-8e7c-5ae1d6bfb07a) Co-authored-by: Scout --- src/components/EnhancedChatInterface.tsx | 159 +++++- src/components/GitHubIntegration.tsx | 615 +++++++++++++++++++++++ src/lib/github-service.ts | 355 +++++++++++++ 3 files changed, 1125 insertions(+), 4 deletions(-) create mode 100644 src/components/GitHubIntegration.tsx create mode 100644 src/lib/github-service.ts diff --git a/src/components/EnhancedChatInterface.tsx b/src/components/EnhancedChatInterface.tsx index 6369131c..fe1cfafb 100644 --- a/src/components/EnhancedChatInterface.tsx +++ b/src/components/EnhancedChatInterface.tsx @@ -34,7 +34,9 @@ import { ArrowUp, Mic, Paperclip, - Settings + Settings, + Github, + GitBranch } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useQuery, useMutation } from 'convex/react'; @@ -52,6 +54,9 @@ import { toast } from 'sonner'; import * as Sentry from '@sentry/react'; import WebContainerFailsafe from './WebContainerFailsafe'; import { DECISION_PROMPT_NEXT } from '@/lib/decisionPrompt'; +import { GitHubIntegration } from '@/components/GitHubIntegration'; +import type { GitHubRepo } from '@/lib/github-service'; +import { githubService } from '@/lib/github-service'; const { logger } = Sentry; @@ -152,6 +157,11 @@ const EnhancedChatInterface: React.FC = () => { // Enhanced animations const [messageAnimations, setMessageAnimations] = useState<{ [key: string]: boolean }>({}); + + // GitHub Integration state + const [selectedRepo, setSelectedRepo] = useState(null); + const [githubContext, setGithubContext] = useState(''); + const [isGithubMode, setIsGithubMode] = useState(false); const textareaRef = useRef(null); const messagesEndRef = useRef(null); @@ -209,6 +219,9 @@ const EnhancedChatInterface: React.FC = () => { const sanitizedInput = sanitizeText(input); if (!sanitizedInput.trim()) return; + + // Enhance message with GitHub context if available + const enhancedInput = await enhanceMessageWithGitHub(sanitizedInput); if (!sessionStarted) { setSessionStarted(true); @@ -228,9 +241,12 @@ const EnhancedChatInterface: React.FC = () => { // Add user message await addMessageMutation({ chatId: currentChatId as Id<'chats'>, - content: sanitizedInput, + content: sanitizedInput, // Store original user input role: 'user', - metadata: {} + metadata: { + githubMode: isGithubMode, + repository: selectedRepo?.full_name + } }); setInput(''); @@ -238,7 +254,7 @@ const EnhancedChatInterface: React.FC = () => { // Generate AI response with enhanced error handling try { - const stream = await streamAIResponse(sanitizedInput); + const stream = await streamAIResponse(enhancedInput); // Use enhanced input for AI let assistantResponse = ''; // Add assistant message placeholder @@ -430,6 +446,78 @@ const EnhancedChatInterface: React.FC = () => { }); }; + // GitHub Integration Functions + const handleRepoSelected = (repo: GitHubRepo) => { + setSelectedRepo(repo); + setIsGithubMode(true); + + const repoContext = `Repository: ${repo.full_name}\n` + + `Description: ${repo.description || 'No description'}\n` + + `Language: ${repo.language || 'Unknown'}\n` + + `Default Branch: ${repo.default_branch}\n` + + `Type: ${repo.private ? 'Private' : 'Public'} repository\n\n`; + + setGithubContext(repoContext); + + // Add context to the current input + setInput(prev => { + const newInput = `I'm working with this GitHub repository:\n\n${repoContext}` + + `Please help me analyze and suggest improvements for this codebase. ` + + `${prev ? '\n\n' + prev : ''}`; + return newInput; + }); + + toast.success(`Repository ${repo.full_name} loaded for AI analysis!`); + }; + + const handlePullRequestCreated = (prUrl: string, repo: GitHubRepo) => { + toast.success('Pull request created successfully!'); + + // Add success message to chat + if (selectedChatId) { + addMessageMutation({ + chatId: selectedChatId as Id<'chats'>, + content: `✅ **Pull Request Created Successfully!**\n\n` + + `Repository: ${repo.full_name}\n` + + `Pull Request: [View PR](${prUrl})\n\n` + + `The changes have been applied and are ready for review. ` + + `You can now review the pull request on GitHub and merge it when ready.`, + role: 'assistant', + metadata: { + type: 'github_success', + prUrl, + repository: repo.full_name + } + }).catch(error => { + logger.error('Failed to add PR success message:', error); + }); + } + }; + + const detectGithubUrls = (text: string): string[] => { + const githubUrlRegex = /https?:\/\/github\.com\/[\w\-\.]+\/[\w\-\.]+(?:\/[^\s]*)?/g; + return text.match(githubUrlRegex) || []; + }; + + const enhanceMessageWithGitHub = async (message: string): Promise => { + let enhancedMessage = message; + + // If GitHub mode is active, add repository context + if (isGithubMode && selectedRepo && githubContext) { + enhancedMessage = githubContext + '\n' + message; + } + + // Detect and suggest GitHub integration + const githubUrls = detectGithubUrls(message); + if (githubUrls.length > 0 && !isGithubMode) { + enhancedMessage += '\n\n[Assistant Note: I detected GitHub repository URLs in your message. ' + + 'Would you like to use the GitHub integration to analyze the repository, ' + + 'make changes, and create pull requests directly?]'; + } + + return enhancedMessage; + }; + if (authLoading) { return (
@@ -1048,6 +1136,64 @@ const EnhancedChatInterface: React.FC = () => {
+ {/* GitHub Mode Indicator */} + + {isGithubMode && selectedRepo && ( + +
+
+
+
+ + GitHub Mode Active +
+
+ {selectedRepo.owner.login} + {selectedRepo.full_name} + + {selectedRepo.language || 'Unknown'} + +
+
+
+ + +
+
+
+
+ )} +
+ {/* Enhanced Input Area */}
@@ -1092,6 +1238,11 @@ const EnhancedChatInterface: React.FC = () => { Clone +
diff --git a/src/components/GitHubIntegration.tsx b/src/components/GitHubIntegration.tsx new file mode 100644 index 00000000..89472856 --- /dev/null +++ b/src/components/GitHubIntegration.tsx @@ -0,0 +1,615 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { Label } from '@/components/ui/label'; +import { + GitBranch, + GitFork, + GitPullRequest, + Github, + ExternalLink, + Loader2, + Check, + AlertCircle, + Settings, + Key, + FileText, + Folder, + Plus +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { githubService, initializeGitHub, type GitHubRepo, type FileChange } from '@/lib/github-service'; +import * as Sentry from '@sentry/react'; + +const { logger } = Sentry; + +export interface GitHubIntegrationProps { + onRepoSelected?: (repo: GitHubRepo) => void; + onPullRequestCreated?: (prUrl: string, repo: GitHubRepo) => void; + className?: string; +} + +interface GitHubOperationStatus { + stage: 'idle' | 'parsing' | 'forking' | 'creating-branch' | 'applying-changes' | 'creating-pr' | 'completed' | 'error'; + message: string; + progress: number; +} + +export function GitHubIntegration({ + onRepoSelected, + onPullRequestCreated, + className = '' +}: GitHubIntegrationProps) { + const [isOpen, setIsOpen] = useState(false); + const [githubUrl, setGithubUrl] = useState(''); + const [isTokenSetup, setIsTokenSetup] = useState(false); + const [githubToken, setGithubToken] = useState(''); + const [currentRepo, setCurrentRepo] = useState(null); + const [operationStatus, setOperationStatus] = useState({ + stage: 'idle', + message: 'Ready to start', + progress: 0 + }); + + // Pull Request creation fields + const [prTitle, setPrTitle] = useState(''); + const [prDescription, setPrDescription] = useState(''); + const [changes, setChanges] = useState([]); + const [showPRForm, setShowPRForm] = useState(false); + + // Token setup + const [showTokenSetup, setShowTokenSetup] = useState(false); + + useEffect(() => { + checkGitHubSetup(); + }, []); + + const checkGitHubSetup = async () => { + const isSetup = await initializeGitHub(); + setIsTokenSetup(isSetup); + }; + + const saveGitHubToken = () => { + if (!githubToken.trim()) { + toast.error('Please enter a valid GitHub token'); + return; + } + + if (!githubService.validateGitHubToken(githubToken)) { + toast.error('Invalid GitHub token format. Please check your token.'); + return; + } + + localStorage.setItem('github_access_token', githubToken.trim()); + githubService.setToken(githubToken.trim()); + setIsTokenSetup(true); + setShowTokenSetup(false); + toast.success('GitHub token saved successfully!'); + }; + + const parseAndLoadRepo = async () => { + if (!githubUrl.trim()) { + toast.error('Please enter a GitHub repository URL'); + return; + } + + if (!isTokenSetup) { + toast.error('Please configure your GitHub token first'); + setShowTokenSetup(true); + return; + } + + try { + setOperationStatus({ + stage: 'parsing', + message: 'Parsing GitHub URL...', + progress: 10 + }); + + const parsed = await githubService.parseRepoUrl(githubUrl); + if (!parsed) { + throw new Error('Invalid GitHub URL format'); + } + + setOperationStatus({ + stage: 'parsing', + message: `Loading repository ${parsed.owner}/${parsed.repo}...`, + progress: 30 + }); + + const repo = await githubService.getRepo(parsed.owner, parsed.repo); + setCurrentRepo(repo); + + setOperationStatus({ + stage: 'completed', + message: `Repository loaded successfully!`, + progress: 100 + }); + + if (onRepoSelected) { + onRepoSelected(repo); + } + + toast.success(`Repository ${repo.full_name} loaded successfully!`); + } catch (error) { + logger.error('Error loading repository:', error); + setOperationStatus({ + stage: 'error', + message: error instanceof Error ? error.message : 'Failed to load repository', + progress: 0 + }); + toast.error(error instanceof Error ? error.message : 'Failed to load repository'); + } + }; + + const createPullRequest = async () => { + if (!currentRepo || changes.length === 0) { + toast.error('No changes to commit'); + return; + } + + if (!prTitle.trim()) { + toast.error('Please enter a pull request title'); + return; + } + + try { + const originalOwner = currentRepo.owner.login; + const originalRepo = currentRepo.name; + + setOperationStatus({ + stage: 'forking', + message: 'Forking repository...', + progress: 20 + }); + + // Fork the repository + const forkedRepo = await githubService.forkRepo(originalOwner, originalRepo); + const userLogin = forkedRepo.owner.login; + + setOperationStatus({ + stage: 'creating-branch', + message: 'Creating feature branch...', + progress: 40 + }); + + // Create a new branch + const branchName = githubService.generateBranchName(prTitle); + await githubService.createBranch(userLogin, originalRepo, branchName, currentRepo.default_branch); + + setOperationStatus({ + stage: 'applying-changes', + message: 'Applying changes...', + progress: 60 + }); + + // Apply changes + await githubService.updateFiles( + userLogin, + originalRepo, + branchName, + changes, + prTitle + ); + + setOperationStatus({ + stage: 'creating-pr', + message: 'Creating pull request...', + progress: 80 + }); + + // Create pull request + const pr = await githubService.createPullRequest( + originalOwner, + originalRepo, + prTitle, + prDescription, + branchName, + currentRepo.default_branch, + originalOwner + ); + + setOperationStatus({ + stage: 'completed', + message: 'Pull request created successfully!', + progress: 100 + }); + + if (onPullRequestCreated) { + onPullRequestCreated(pr.html_url, currentRepo); + } + + toast.success(`Pull request created: ${pr.html_url}`); + + // Reset form + setShowPRForm(false); + setPrTitle(''); + setPrDescription(''); + setChanges([]); + + } catch (error) { + logger.error('Error creating pull request:', error); + setOperationStatus({ + stage: 'error', + message: error instanceof Error ? error.message : 'Failed to create pull request', + progress: 0 + }); + toast.error(error instanceof Error ? error.message : 'Failed to create pull request'); + } + }; + + const addFileChange = () => { + setChanges([...changes, { path: '', content: '', action: 'create' }]); + }; + + const updateFileChange = (index: number, field: keyof FileChange, value: string) => { + const updatedChanges = [...changes]; + updatedChanges[index] = { ...updatedChanges[index], [field]: value }; + setChanges(updatedChanges); + }; + + const removeFileChange = (index: number) => { + setChanges(changes.filter((_, i) => i !== index)); + }; + + const getStatusIcon = () => { + switch (operationStatus.stage) { + case 'parsing': + case 'forking': + case 'creating-branch': + case 'applying-changes': + case 'creating-pr': + return ; + case 'completed': + return ; + case 'error': + return ; + default: + return ; + } + }; + + return ( +
+ + + + + + + + + GitHub Integration + + + +
+ {/* GitHub Token Setup */} + + + + + GitHub Token Setup + {isTokenSetup && Configured} + + + + {!isTokenSetup ? ( +
+

+ You need a GitHub Personal Access Token to fork repositories and create pull requests. +

+
+ + +
+ + {showTokenSetup && ( + + + setGithubToken(e.target.value)} + className="font-mono" + /> +

+ Required scopes: repo, workflow, write:packages +

+ +
+ )} +
+ ) : ( +
+ + GitHub token configured successfully + +
+ )} +
+
+ + {/* Repository Input */} + + + + + Repository Selection + + + +
+ +
+ setGithubUrl(e.target.value)} + disabled={operationStatus.stage !== 'idle' && operationStatus.stage !== 'completed' && operationStatus.stage !== 'error'} + /> + +
+
+ + {/* Operation Status */} + + {operationStatus.stage !== 'idle' && ( + +
+ {getStatusIcon()} + {operationStatus.message} +
+
+
+
+ + )} + + + {/* Repository Info */} + + {currentRepo && ( + +
+ {currentRepo.owner.login} +
+

{currentRepo.full_name}

+

{currentRepo.description}

+
+
+ {currentRepo.language && ( + {currentRepo.language} + )} + + {currentRepo.private ? 'Private' : 'Public'} + +
+
+ +
+ + +
+
+ )} +
+ + + + {/* Pull Request Form */} + + {showPRForm && currentRepo && ( + + + + + + Create Pull Request + + + +
+
+ + setPrTitle(e.target.value)} + /> +
+ +
+ +