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,34 @@
"use client";
import { WorkspaceLeftRail } from "@/components/layout/workspace-left-rail";
import { RightPanel } from "@/components/layout/right-panel";
import { ReactNode, useState } from "react";
import { Toaster } from "sonner";
export default function KeysLayout({
children,
}: {
children: ReactNode;
}) {
const [activeSection, setActiveSection] = useState<string>("keys");
return (
<>
<div className="flex h-screen w-full overflow-hidden bg-background">
{/* Left Rail - Workspace Navigation */}
<WorkspaceLeftRail activeSection={activeSection} onSectionChange={setActiveSection} />
{/* Main Content Area */}
<main className="flex-1 flex flex-col overflow-hidden">
{children}
</main>
{/* Right Panel - AI Chat */}
<RightPanel />
</div>
<Toaster position="top-center" />
</>
);
}

View File

@@ -0,0 +1,412 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Key, Plus, Trash2, Eye, EyeOff, ExternalLink, Save } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface ApiKey {
id: string;
service: string;
name: string;
createdAt: any;
lastUsed: any;
}
const SUPPORTED_SERVICES = [
{
id: 'openai',
name: 'OpenAI',
description: 'For ChatGPT imports and AI features',
placeholder: 'sk-...',
helpUrl: 'https://platform.openai.com/api-keys',
},
{
id: 'github',
name: 'GitHub',
description: 'Personal access token for repository access',
placeholder: 'ghp_...',
helpUrl: 'https://github.com/settings/tokens',
},
{
id: 'anthropic',
name: 'Anthropic (Claude)',
description: 'For Claude AI integrations',
placeholder: 'sk-ant-...',
helpUrl: 'https://console.anthropic.com/settings/keys',
},
];
export default function KeysPage() {
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [showAddDialog, setShowAddDialog] = useState(false);
const [selectedService, setSelectedService] = useState('');
const [keyValue, setKeyValue] = useState('');
const [showKey, setShowKey] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadKeys();
}, []);
const loadKeys = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setKeys(data.keys);
}
} catch (error) {
console.error('Error loading keys:', error);
toast.error('Failed to load API keys');
} finally {
setLoading(false);
}
};
const handleAddKey = async () => {
if (!selectedService || !keyValue) {
toast.error('Please select a service and enter a key');
return;
}
setSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
const token = await user.getIdToken();
const service = SUPPORTED_SERVICES.find(s => s.id === selectedService);
const response = await fetch('/api/keys', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
service: selectedService,
name: service?.name,
keyValue,
}),
});
if (response.ok) {
toast.success(`${service?.name} key saved successfully`);
setShowAddDialog(false);
setSelectedService('');
setKeyValue('');
loadKeys();
} else {
const error = await response.json();
toast.error(error.error || 'Failed to save key');
}
} catch (error) {
console.error('Error saving key:', error);
toast.error('Failed to save key');
} finally {
setSaving(false);
}
};
const handleDeleteKey = async (service: string, name: string) => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ service }),
});
if (response.ok) {
toast.success(`${name} key deleted`);
loadKeys();
} else {
toast.error('Failed to delete key');
}
} catch (error) {
console.error('Error deleting key:', error);
toast.error('Failed to delete key');
}
};
const getServiceConfig = (serviceId: string) => {
return SUPPORTED_SERVICES.find(s => s.id === serviceId);
};
return (
<div className="flex h-full flex-col overflow-auto">
<div className="flex-1 p-8 space-y-8 max-w-4xl">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold mb-2">API Keys</h1>
<p className="text-muted-foreground text-lg">
Manage your third-party API keys for Vibn integrations
</p>
</div>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Key
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add API Key</DialogTitle>
<DialogDescription>
Add a third-party API key for Vibn to use on your behalf
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="service">Service</Label>
<Select value={selectedService} onValueChange={setSelectedService}>
<SelectTrigger>
<SelectValue placeholder="Select a service" />
</SelectTrigger>
<SelectContent>
{SUPPORTED_SERVICES.map(service => (
<SelectItem key={service.id} value={service.id}>
{service.name}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedService && (
<p className="text-xs text-muted-foreground">
{getServiceConfig(selectedService)?.description}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="key">API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="key"
type={showKey ? 'text' : 'password'}
placeholder={getServiceConfig(selectedService)?.placeholder || 'Enter API key'}
value={keyValue}
onChange={(e) => setKeyValue(e.target.value)}
className="pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowKey(!showKey)}
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{selectedService && (
<a
href={getServiceConfig(selectedService)?.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Get your API key <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
<div className="rounded-lg border bg-muted/50 p-3">
<p className="text-sm text-muted-foreground">
<strong>🔐 Secure Storage:</strong> Your API key will be encrypted and stored securely.
Vibn will only use it when you explicitly request actions that require it.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddKey} disabled={saving || !selectedService || !keyValue}>
{saving ? 'Saving...' : 'Save Key'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Keys List */}
{loading ? (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading your API keys...</p>
</CardContent>
</Card>
) : keys.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center space-y-4">
<div className="flex justify-center">
<div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Key className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">No API keys yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Add your third-party API keys to enable Vibn features like ChatGPT imports and AI analysis
</p>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
Add Your First Key
</Button>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{keys.map((key) => {
const serviceConfig = getServiceConfig(key.service);
return (
<Card key={key.id}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Key className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-base">{key.name}</CardTitle>
<CardDescription>
{serviceConfig?.description || key.service}
</CardDescription>
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete API Key?</AlertDialogTitle>
<AlertDialogDescription>
This will remove your {key.name} API key. Features using this key will stop working until you add a new one.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDeleteKey(key.service, key.name)}>
Delete Key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<div className="space-y-1">
<p className="text-muted-foreground">
Added: {key.createdAt ? new Date(key.createdAt._seconds * 1000).toLocaleDateString() : 'Unknown'}
</p>
{key.lastUsed && (
<p className="text-muted-foreground">
Last used: {new Date(key.lastUsed._seconds * 1000).toLocaleDateString()}
</p>
)}
</div>
{serviceConfig && (
<a
href={serviceConfig.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Manage on {serviceConfig.name} <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{/* Info Card */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="text-base">How API Keys Work</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
<strong>🔐 Encrypted Storage:</strong> All API keys are encrypted before being stored in the database.
</p>
<p>
<strong>🎯 Automatic Usage:</strong> When you use Vibn features (like ChatGPT import), we'll automatically use your stored keys instead of asking each time.
</p>
<p>
<strong>🔄 Easy Updates:</strong> Add a new key with the same service name to replace an existing one.
</p>
<p>
<strong>🗑 Full Control:</strong> Delete keys anytime - you can always add them back later.
</p>
</CardContent>
</Card>
</div>
</div>
);
}