VIBN Frontend for Coolify deployment
This commit is contained in:
34
app/[workspace]/keys/layout.tsx
Normal file
34
app/[workspace]/keys/layout.tsx
Normal 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" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
412
app/[workspace]/keys/page.tsx
Normal file
412
app/[workspace]/keys/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user