Files
gemini-nexus/src/components/proxy/advanced/ReplayTab.tsx
T
Gemini Nexus Bot fd1b2b8bc0 fix: resolve TypeScript type-check errors
- Add cacheHitRate to ProxyMetricsService, handler, and router schema
- Fix StatCard to support string values and purple accent
- Fix duplicate tabs keys in i18n translations (EN/ZH/RU)
- Add missing cacheStatus translations across all languages
2026-04-23 02:50:09 -03:00

174 lines
6.0 KiB
TypeScript

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ipc } from '@/ipc/manager';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { Loader2, Play, RefreshCw, Clock, AlertCircle, CheckCircle } from 'lucide-react';
interface ReplayEntry {
id: string;
timestamp: number;
method: string;
endpoint: string;
status?: number;
duration?: number;
body?: unknown;
}
export const ReplayTab: React.FC = () => {
const { t } = useTranslation();
const { toast } = useToast();
const queryClient = useQueryClient();
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
const {
data: requestsData,
isLoading: requestsLoading,
refetch: refetchRequests,
} = useQuery({
queryKey: ['proxyAdvanced', 'recentRequests'],
queryFn: () => ipc.client.proxyAdvanced.getRecentRequests(),
refetchInterval: 10000,
});
const replayMutation = useMutation({
mutationFn: (requestId: string) => ipc.client.proxyAdvanced.replayRequest({ requestId }),
onSuccess: (data) => {
if (data.success) {
toast({
title: t('proxy.advanced.replay.success'),
description: t('proxy.advanced.replay.replayedSuccessfully'),
});
} else {
toast({
title: t('proxy.advanced.replay.error'),
description: data.error || t('proxy.advanced.replay.unknownError'),
variant: 'destructive',
});
}
setSelectedRequestId(null);
},
onError: (error) => {
toast({
title: t('proxy.advanced.replay.error'),
description: error instanceof Error ? error.message : t('proxy.advanced.replay.unknownError'),
variant: 'destructive',
});
setSelectedRequestId(null);
},
});
const requests: ReplayEntry[] = requestsData?.success ? (requestsData.data as ReplayEntry[]) : [];
const handleReplay = (requestId: string) => {
setSelectedRequestId(requestId);
replayMutation.mutate(requestId);
};
const formatTimestamp = (timestamp: number): string => {
return new Date(timestamp).toLocaleTimeString();
};
const getStatusBadge = (status?: number) => {
if (!status) return <Badge variant="outline">{t('proxy.advanced.replay.pending')}</Badge>;
if (status >= 200 && status < 300) {
return (
<Badge variant="default" className="bg-green-600 hover:bg-green-700">
<CheckCircle className="mr-1 h-3 w-3" />
{status}
</Badge>
);
}
if (status >= 400) {
return (
<Badge variant="destructive">
<AlertCircle className="mr-1 h-3 w-3" />
{status}
</Badge>
);
}
return <Badge variant="secondary">{status}</Badge>;
};
return (
<div className="space-y-5">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">{t('proxy.advanced.replay.title')}</h3>
<p className="text-sm text-muted-foreground">{t('proxy.advanced.replay.description')}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => refetchRequests()}
disabled={requestsLoading}
>
<RefreshCw className={`mr-2 h-4 w-4 ${requestsLoading ? 'animate-spin' : ''}`} />
{t('proxy.advanced.replay.refresh')}
</Button>
</div>
{/* Requests List */}
<Card>
<CardHeader>
<CardTitle>{t('proxy.advanced.replay.recentRequests')}</CardTitle>
<CardDescription>{t('proxy.advanced.replay.clickToReplay')}</CardDescription>
</CardHeader>
<CardContent>
{requestsLoading ? (
<div className="flex h-32 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : requests.length === 0 ? (
<div className="flex h-32 flex-col items-center justify-center gap-2 rounded-lg border border-dashed">
<Clock className="h-8 w-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{t('proxy.advanced.replay.noRequests')}
</p>
</div>
) : (
<div className="space-y-2">
{requests.map((request) => (
<div
key={request.id}
className="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-3">
{getStatusBadge(request.status)}
<div className="flex flex-col">
<span className="text-sm font-medium">
{request.method} {request.endpoint}
</span>
<span className="text-xs text-muted-foreground">
{formatTimestamp(request.timestamp)}
{request.duration && ` · ${request.duration}ms`}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleReplay(request.id)}
disabled={replayMutation.isPending && selectedRequestId === request.id}
>
{replayMutation.isPending && selectedRequestId === request.id ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Play className="mr-2 h-4 w-4" />
)}
{t('proxy.advanced.replay.replay')}
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
};