VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View 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>
);
}