126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import { Button } from '@/components/ui/button';
|
|
import { Check, Loader2, ShieldAlert, X } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
|
|
interface RoomAiAuthRequest {
|
|
auth_uid: string;
|
|
tool_name: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface RoomAiAuthBannerProps {
|
|
pendingAuthRequests: RoomAiAuthRequest[];
|
|
isAdmin: boolean;
|
|
onDecide: (authUid: string, approved: boolean) => Promise<void> | void;
|
|
}
|
|
|
|
function formatTimeLeft(createdAt: string): string {
|
|
const created = new Date(createdAt).getTime();
|
|
const now = Date.now();
|
|
const elapsed = Math.floor((now - created) / 1000);
|
|
const remaining = Math.max(0, 300 - elapsed);
|
|
const minutes = Math.floor(remaining / 60);
|
|
const seconds = remaining % 60;
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function AuthRequestItem({
|
|
request,
|
|
isAdmin,
|
|
onDecide,
|
|
}: {
|
|
request: RoomAiAuthRequest;
|
|
isAdmin: boolean;
|
|
onDecide: (authUid: string, approved: boolean) => Promise<void> | void;
|
|
}) {
|
|
const [timeLeft, setTimeLeft] = useState(() => formatTimeLeft(request.created_at));
|
|
const [isPending, setIsPending] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setTimeLeft(formatTimeLeft(request.created_at));
|
|
}, 1000);
|
|
return () => clearInterval(interval);
|
|
}, [request.created_at]);
|
|
|
|
const handleDecide = async (approved: boolean) => {
|
|
setIsPending(true);
|
|
try {
|
|
await onDecide(request.auth_uid, approved);
|
|
toast.success(approved ? 'Function call approved' : 'Function call denied');
|
|
} catch {
|
|
toast.error(approved ? 'Failed to approve' : 'Failed to deny');
|
|
} finally {
|
|
setIsPending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-3 rounded-md border border-amber-300/50 bg-amber-50/80 px-3 py-2 dark:border-amber-700/50 dark:bg-amber-950/30">
|
|
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">
|
|
AI wants to call{' '}
|
|
<code className="rounded bg-amber-200/60 px-1 py-0.5 font-mono text-[11px] dark:bg-amber-800/40">
|
|
{request.tool_name}
|
|
</code>
|
|
</p>
|
|
{!isAdmin && (
|
|
<p className="mt-0.5 text-[11px] text-amber-600/80 dark:text-amber-400/70">
|
|
Waiting for admin approval...
|
|
</p>
|
|
)}
|
|
</div>
|
|
<span className="shrink-0 font-mono text-[11px] tabular-nums text-amber-600/70 dark:text-amber-400/60">
|
|
{timeLeft}
|
|
</span>
|
|
{isAdmin && (
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 text-green-600 hover:bg-green-100 hover:text-green-700 dark:text-green-400 dark:hover:bg-green-900/30"
|
|
onClick={() => void handleDecide(true)}
|
|
disabled={isPending}
|
|
title="Approve"
|
|
>
|
|
{isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 text-red-600 hover:bg-red-100 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-900/30"
|
|
onClick={() => void handleDecide(false)}
|
|
disabled={isPending}
|
|
title="Deny"
|
|
>
|
|
{isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function RoomAiAuthBanner({
|
|
pendingAuthRequests,
|
|
isAdmin,
|
|
onDecide,
|
|
}: RoomAiAuthBannerProps) {
|
|
if (pendingAuthRequests.length === 0) return null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5 border-t border-border/70 bg-background/95 px-4 py-2">
|
|
{pendingAuthRequests.map((request) => (
|
|
<AuthRequestItem
|
|
key={request.auth_uid}
|
|
request={request}
|
|
isAdmin={isAdmin}
|
|
onDecide={onDecide}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|