270 lines
8.7 KiB
TypeScript
270 lines
8.7 KiB
TypeScript
'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>
|
||
);
|
||
}
|
||
|