gitdataai/src/components/room/RoomAiAuthBanner.tsx
2026-04-15 09:08:09 +08:00

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