Files
vibn-frontend/components/ai/github-repo-picker.tsx

270 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Github, Loader2, CheckCircle2, ExternalLink, X } from 'lucide-react';
import { auth } from '@/lib/firebase/config';
import { doc, updateDoc, serverTimestamp } from 'firebase/firestore';
import { db } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { initiateGitHubOAuth } from '@/lib/github/oauth';
interface GitHubRepoPickerProps {
projectId: string;
onRepoSelected?: (repo: any) => void;
onClose?: () => void;
}
export function GitHubRepoPicker({ projectId, onRepoSelected, onClose }: GitHubRepoPickerProps) {
const [loading, setLoading] = useState(false);
const [connected, setConnected] = useState(false);
const [repos, setRepos] = useState<any[]>([]);
const [selectedRepo, setSelectedRepo] = useState<any>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
checkConnection();
}, []);
const checkConnection = async () => {
setLoading(true);
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const statusResponse = await fetch('/api/github/connect', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (statusResponse.ok) {
const statusData = await statusResponse.json();
setConnected(statusData.connected);
if (statusData.connected) {
const reposResponse = await fetch('/api/github/repos', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (reposResponse.ok) {
const reposData = await reposResponse.json();
setRepos(reposData);
}
}
}
} catch (error) {
console.error('Error checking GitHub connection:', error);
} finally {
setLoading(false);
}
};
const handleConnect = () => {
const redirectUri = `${window.location.origin}/api/github/oauth/callback`;
initiateGitHubOAuth(redirectUri);
};
const handleSelectRepo = async (repo: any) => {
setSelectedRepo(repo);
setSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
// Update project with GitHub info
await updateDoc(doc(db, 'projects', projectId), {
githubRepo: repo.full_name,
githubRepoId: repo.id,
githubRepoUrl: repo.html_url,
githubDefaultBranch: repo.default_branch,
hasGithub: true,
updatedAt: serverTimestamp(),
});
// Try to automatically associate existing sessions with this repo
try {
const token = await user.getIdToken();
const associateResponse = await fetch(`/api/projects/${projectId}/associate-github-sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
githubRepo: repo.full_name,
githubRepoUrl: repo.html_url,
}),
});
if (associateResponse.ok) {
const data = await associateResponse.json();
console.log('🔗 Session association result:', data);
if (data.sessionsAssociated > 0) {
toast.success(`Connected to ${repo.full_name}!`, {
description: `Found and linked ${data.sessionsAssociated} existing chat sessions from this repository`,
duration: 5000,
});
} else {
// No sessions found - show helpful message
toast.success(`Connected to ${repo.full_name}!`, {
description: `Repository linked! Future chat sessions from this repo will be automatically tracked here.`,
duration: 5000,
});
console.log(` No matching sessions found. This could mean:
- No chat sessions exist yet for this repo
- Sessions are already linked to other projects
- Workspace folder name doesn't match repo name (${repo.name})`);
}
} else {
// Connection succeeded but session association failed - still show success
toast.success(`Connected to ${repo.full_name}!`);
console.warn('Session association failed but connection succeeded');
}
} catch (associateError) {
// Don't fail the whole operation if association fails
console.error('Error associating sessions:', associateError);
toast.success(`Connected to ${repo.full_name}!`);
}
// Notify parent component
if (onRepoSelected) {
onRepoSelected(repo);
}
} catch (error) {
console.error('Error connecting repo:', error);
toast.error('Failed to connect repository');
setSelectedRepo(null);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<Card className="my-2">
<CardContent className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</CardContent>
</Card>
);
}
if (!connected) {
return (
<Card className="my-2 border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Github className="h-5 w-5" />
Connect GitHub
</CardTitle>
<CardDescription>
Connect your GitHub account to select a repository
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={handleConnect} className="w-full">
<Github className="mr-2 h-4 w-4" />
Connect GitHub Account
</Button>
</CardContent>
</Card>
);
}
if (selectedRepo) {
return (
<Card className="my-2 border-green-500/50 bg-green-50/50 dark:bg-green-950/20">
<CardContent className="flex items-center gap-3 py-4">
<CheckCircle2 className="h-5 w-5 text-green-600" />
<div className="flex-1">
<p className="font-medium">{selectedRepo.full_name}</p>
<p className="text-sm text-muted-foreground">Repository connected!</p>
</div>
<a
href={selectedRepo.html_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
View
</a>
{onClose && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 ml-2"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
)}
</CardContent>
</Card>
);
}
return (
<Card className="my-2">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Github className="h-5 w-5" />
Select Repository
</CardTitle>
<CardDescription>
Choose which repository to connect to this project
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{repos.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
No repositories found
</p>
) : (
repos.map((repo) => (
<button
key={repo.id}
onClick={() => handleSelectRepo(repo)}
disabled={saving}
className="w-full text-left p-3 rounded-lg border-2 border-border hover:border-primary transition-all disabled:opacity-50"
>
<div className="font-medium">{repo.full_name}</div>
{repo.description && (
<div className="text-sm text-muted-foreground truncate mt-1">
{repo.description}
</div>
)}
<div className="flex items-center gap-2 mt-2">
{repo.language && (
<span className="text-xs bg-muted px-2 py-0.5 rounded">
{repo.language}
</span>
)}
{repo.private && (
<span className="text-xs bg-yellow-500/10 text-yellow-600 px-2 py-0.5 rounded">
Private
</span>
)}
</div>
</button>
))
)}
</div>
</CardContent>
</Card>
);
}