VIBN Frontend for Coolify deployment
This commit is contained in:
269
components/ai/github-repo-picker.tsx
Normal file
269
components/ai/github-repo-picker.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user