axonhub / frontend /src /features /requests /components /request-detail-page.tsx
llzai's picture
Upload 1793 files
9853396 verified
import { useState, useCallback } from 'react';
import { format } from 'date-fns';
import { DashboardIcon } from '@radix-ui/react-icons';
import { useParams, useNavigate } from '@tanstack/react-router';
import { zhCN, enUS } from 'date-fns/locale';
import { Copy, Clock, Key, Database, ArrowLeft, FileText, Layers, Download, Terminal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { extractNumberID } from '@/lib/utils';
import { usePaginationSearch } from '@/hooks/use-pagination-search';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { JsonViewer } from '@/components/json-tree-view';
import { Header } from '@/components/layout/header';
import { Main } from '@/components/layout/main';
import { useGeneralSettings } from '@/features/system/data/system';
import { useUsageLogs } from '../../usage-logs/data/usage-logs';
import { useRequest, useRequestExecutions } from '../data';
import { ChunksDialog } from './chunks-dialog';
import { CurlPreviewDialog } from './curl-preview-dialog';
import { getStatusColor } from './help';
import { generateRequestCurl, generateExecutionCurl } from '../utils/curl-generator';
export default function RequestDetailPage() {
const { t, i18n } = useTranslation();
const { requestId } = useParams({ from: '/_authenticated/project/requests/$requestId' });
const navigate = useNavigate();
const locale = i18n.language === 'zh' ? zhCN : enUS;
const { getSearchParams } = usePaginationSearch({ defaultPageSize: 20 });
const [showResponseChunks, setShowResponseChunks] = useState(false);
const [showExecutionChunks, setShowExecutionChunks] = useState(false);
const [selectedResponseChunks, setSelectedResponseChunks] = useState<any[]>([]);
const [selectedExecutionChunks, setSelectedExecutionChunks] = useState<any[]>([]);
const [showCurlPreview, setShowCurlPreview] = useState(false);
const [curlCommand, setCurlCommand] = useState('');
const { data: settings } = useGeneralSettings();
const { data: request, isLoading } = useRequest(requestId);
const { data: executions } = useRequestExecutions(requestId, {
first: 10,
orderBy: { field: 'CREATED_AT', direction: 'DESC' },
});
const { data: usageLogs } = useUsageLogs({
first: 1,
where: { requestID: requestId },
orderBy: { field: 'CREATED_AT', direction: 'DESC' },
});
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success(t('requests.actions.copy'));
};
const downloadFile = (content: string, filename: string) => {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success(t('requests.actions.download'));
};
const showResponseChunksModal = useCallback(() => {
if (request?.responseChunks) {
setSelectedResponseChunks(request.responseChunks);
setShowResponseChunks(true);
}
}, [request]);
const showExecutionChunksModal = useCallback((chunks: any[]) => {
if (chunks && chunks.length > 0) {
setSelectedExecutionChunks(chunks);
setShowExecutionChunks(true);
}
}, []);
const formatJson = (data: any) => {
if (!data) return '';
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
};
const showRequestCurlPreview = useCallback(
(headers: any, body: any, apiFormat?: string) => {
const curl = generateRequestCurl(headers, body, apiFormat as any);
setCurlCommand(curl);
setShowCurlPreview(true);
},
[]
);
const showExecutionCurlPreview = useCallback(
(headers: any, body: any, channel?: { baseURL?: string; type?: string }, apiFormat?: string) => {
const curl = generateExecutionCurl(headers, body, channel as any, apiFormat as any);
setCurlCommand(curl);
setShowCurlPreview(true);
},
[]
);
const calculateLatency = (createdAt: string | Date, updatedAt: string | Date) => {
if (!createdAt || !updatedAt) return null;
const start = new Date(createdAt).getTime();
const end = new Date(updatedAt).getTime();
const diffMs = end - start;
if (diffMs < 0) return null;
return diffMs;
};
const formatLatency = (latencyMs: number | null) => {
if (latencyMs === null) return t('requests.columns.unknown');
if (latencyMs < 1000) return `${latencyMs}ms`;
return `${(latencyMs / 1000).toFixed(2)}s`;
};
const handleBack = () => {
// 保持分页状态返回到请求列表页
navigate({
to: '/project/requests',
search: getSearchParams(),
});
};
if (isLoading) {
return (
<div className='flex h-screen flex-col'>
<Header className='border-b'></Header>
<Main className='flex-1'>
<div className='flex h-full items-center justify-center'>
<div className='space-y-4 text-center'>
<div className='border-primary mx-auto h-12 w-12 animate-spin rounded-full border-b-2'></div>
<p className='text-muted-foreground text-lg'>{t('common.loading')}</p>
</div>
</div>
</Main>
</div>
);
}
if (!request) {
return (
<div className='flex h-screen flex-col'>
<Header className='border-b'></Header>
<Main className='flex-1'>
<div className='flex h-full items-center justify-center'>
<div className='space-y-6 text-center'>
<div className='space-y-2'>
<FileText className='text-muted-foreground mx-auto h-16 w-16' />
<p className='text-muted-foreground text-xl font-medium'>{t('requests.dialogs.requestDetail.notFound')}</p>
</div>
<Button onClick={handleBack} size='lg'>
<ArrowLeft className='mr-2 h-4 w-4' />
{t('common.back')}
</Button>
</div>
</div>
</Main>
</div>
);
}
return (
<div className='flex h-screen flex-col'>
<Header className='bg-background/95 supports-[backdrop-filter]:bg-background/60 border-b backdrop-blur'>
<div className='flex items-center space-x-4'>
<Button variant='ghost' size='sm' onClick={handleBack} className='hover:bg-accent'>
<ArrowLeft className='mr-2 h-4 w-4' />
{t('common.back')}
</Button>
<Separator orientation='vertical' className='h-6' />
<div className='flex items-center space-x-3'>
<div className='bg-primary/10 flex h-8 w-8 items-center justify-center rounded-lg'>
<FileText className='text-primary h-4 w-4' />
</div>
<div>
<h1 className='text-lg leading-none font-semibold'>
{t('requests.detail.title')} #{extractNumberID(request.id) || request.id}
</h1>
<div className='mt-1 flex items-center gap-2'>
<p className='text-muted-foreground text-sm'>{request.modelID || t('requests.columns.unknown')}</p>
<span className='text-muted-foreground text-xs'></span>
<p className='text-muted-foreground text-xs'>{format(new Date(request.createdAt), 'yyyy-MM-dd HH:mm:ss', { locale })}</p>
</div>
</div>
</div>
</div>
</Header>
<Main className='flex-1 overflow-auto'>
<div className='container mx-auto max-w-7xl space-y-8 p-6'>
{/* Request Overview Card */}
<Card className='border-0 shadow-sm'>
<CardHeader className='pb-2'>
<CardTitle className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='bg-primary/10 flex h-7 w-7 items-center justify-center rounded-lg'>
<DashboardIcon className='text-primary h-3.5 w-3.5' />
</div>
<span className='text-base'>{t('requests.detail.overview')}</span>
</div>
<Badge className={getStatusColor(request.status)} variant='secondary'>
{t(`requests.status.${request.status}`)}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className='grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3'>
<div className='bg-muted/30 flex items-center justify-between gap-2 rounded-lg border px-3 py-2'>
<div className='flex items-center gap-2'>
<Database className='text-primary h-3.5 w-3.5' />
<span className='text-xs font-medium'>{t('requests.columns.channel')}</span>
</div>
<p className='bg-background rounded border px-2 py-0.5 font-mono text-xs'>
{request.channel?.name || t('requests.columns.unknown')}
</p>
</div>
<div className='bg-muted/30 flex items-center justify-between gap-2 rounded-lg border px-3 py-2'>
<div className='flex items-center gap-2'>
<Database className='text-primary h-3.5 w-3.5' />
<span className='text-xs font-medium'>{t('requests.columns.modelId')}</span>
</div>
<p className='bg-background rounded border px-2 py-0.5 font-mono text-xs'>
{request.modelID || t('requests.columns.unknown')}
</p>
</div>
<div className='bg-muted/30 flex items-center justify-between gap-2 rounded-lg border px-3 py-2'>
<div className='flex items-center gap-2'>
<Key className='text-primary h-3.5 w-3.5' />
<span className='text-xs font-medium'>{t('requests.dialogs.requestDetail.fields.apiKeyName')}</span>
</div>
<p className='text-muted-foreground font-mono text-xs'>{request.apiKey?.name || t('requests.columns.unknown')}</p>
</div>
</div>
</CardContent>
</Card>
{/* Usage Card */}
{usageLogs &&
usageLogs.edges.length > 0 &&
(() => {
const usage = usageLogs.edges[0].node;
const promptTokens = usage.promptTokens || 0;
const cachedTokens = usage.promptCachedTokens || 0;
const writeCachedTokens = usage.promptWriteCachedTokens || 0;
const hasReadCache = cachedTokens > 0;
const hasWriteCache = writeCachedTokens > 0;
const cacheHitRate = hasReadCache ? ((cachedTokens / promptTokens) * 100).toFixed(1) : '0.0';
const writeCacheRate = hasWriteCache ? ((writeCachedTokens / promptTokens) * 100).toFixed(1) : '0.0';
const cost = usage.totalCost ?? 0;
const promptCost = usage.costItems?.find((i: any) => i.itemCode === 'prompt_tokens')?.subtotal;
const completionCost = usage.costItems?.find((i: any) => i.itemCode === 'completion_tokens')?.subtotal;
const cacheReadCost = usage.costItems?.find((i: any) => i.itemCode === 'prompt_cached_tokens')?.subtotal;
const cacheWriteCost = usage.costItems?.find((i: any) => i.itemCode === 'prompt_write_cached_tokens')?.subtotal;
const formatCurrency = (val: number) =>
t('currencies.format', {
val,
currency: settings?.currencyCode,
locale: i18n.language === 'zh' ? 'zh-CN' : 'en-US',
minimumFractionDigits: 6,
});
const renderCost = (val: number | null | undefined) => {
if (cost <= 0) return '-';
if (val == null || val <= 0) return '-';
return formatCurrency(val);
};
return (
<Card className='border-0 shadow-sm'>
<CardHeader className='pb-2'>
<CardTitle className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='bg-primary/10 flex h-7 w-7 items-center justify-center rounded-lg'>
<Database className='text-primary h-3.5 w-3.5' />
</div>
<span className='text-base'>{t('requests.detail.tabs.usage')}</span>
</div>
<Badge className='bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' variant='secondary'>
{t(`usageLogs.source.${usage.source}`)}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className='grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5'>
<div className='bg-muted/30 flex flex-col justify-center rounded-lg border px-2.5 py-2'>
<span className='text-muted-foreground text-xs font-medium'>{t('usageLogs.columns.inputLabel')}</span>
<div className='mt-1'>
<p className='text-sm font-semibold'>{usage.promptTokens.toLocaleString()}</p>
<p className='text-muted-foreground text-xs'>{renderCost(promptCost)}</p>
</div>
</div>
<div className='bg-muted/30 flex flex-col justify-center rounded-lg border px-2.5 py-2'>
<span className='text-muted-foreground text-xs font-medium'>{t('usageLogs.columns.outputLabel')}</span>
<div className='mt-1'>
<p className='text-sm font-semibold'>{usage.completionTokens.toLocaleString()}</p>
<p className='text-muted-foreground text-xs'>{renderCost(completionCost)}</p>
</div>
</div>
<div className='bg-muted/30 flex flex-col justify-center rounded-lg border px-2.5 py-2'>
<span className='text-muted-foreground text-xs font-medium'>{t('usageLogs.columns.promptCachedTokens')}</span>
<div className='mt-1'>
<div className='flex items-center justify-between'>
<p className='text-sm font-semibold'>{cachedTokens.toLocaleString()}</p>
{hasReadCache && (
<Badge variant='outline' className='h-4 px-1 text-[10px] text-green-600 border-green-200 bg-green-50'>
{cacheHitRate}%
</Badge>
)}
</div>
<p className='text-muted-foreground text-xs'>{renderCost(cacheReadCost)}</p>
</div>
</div>
<div className='bg-muted/30 flex flex-col justify-center rounded-lg border px-2.5 py-2'>
<span className='text-muted-foreground text-xs font-medium'>{t('usageLogs.columns.writeCacheTokens')}</span>
<div className='mt-1'>
<div className='flex items-center justify-between'>
<p className='text-sm font-semibold'>{writeCachedTokens.toLocaleString()}</p>
{hasWriteCache && (
<Badge variant='outline' className='h-4 px-1 text-[10px] text-blue-600 border-blue-200 bg-blue-50'>
{writeCacheRate}%
</Badge>
)}
</div>
<p className='text-muted-foreground text-xs'>{renderCost(cacheWriteCost)}</p>
</div>
</div>
<div className='bg-muted/30 flex flex-col justify-center rounded-lg border px-2.5 py-2'>
<span className='text-muted-foreground text-xs font-medium'>{t('usageLogs.columns.totalTokens')}</span>
<div className='mt-1'>
<p className='text-sm font-semibold'>{usage.totalTokens.toLocaleString()}</p>
<p className='text-muted-foreground text-xs'>{renderCost(cost)}</p>
</div>
</div>
</div>
</CardContent>
</Card>
);
})()}
{/* Request and Response Tabs */}
<Card className='border-0 shadow-sm'>
<CardContent className='p-0'>
<Tabs defaultValue='request' className='w-full'>
<div className='bg-muted/20 border-b px-6 pt-6'>
<TabsList className='bg-background grid w-full grid-cols-3'>
<TabsTrigger value='request' className='data-[state=active]:bg-primary data-[state=active]:text-primary-foreground'>
{t('requests.detail.tabs.request')}
</TabsTrigger>
<TabsTrigger value='response' className='data-[state=active]:bg-primary data-[state=active]:text-primary-foreground'>
{t('requests.detail.tabs.response')}
</TabsTrigger>
<TabsTrigger value='executions' className='data-[state=active]:bg-primary data-[state=active]:text-primary-foreground'>
{t('requests.detail.tabs.executions')}
</TabsTrigger>
</TabsList>
</div>
<TabsContent value='request' className='space-y-6 p-6'>
<div className='flex justify-end'>
<Button
variant='outline'
size='sm'
onClick={() => showRequestCurlPreview(request.requestHeaders, request.requestBody, request.format)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Terminal className='mr-2 h-4 w-4' />
{t('requests.actions.copyCurl')}
</Button>
</div>
{request.requestHeaders && (
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<h4 className='flex items-center gap-2 text-base font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.requestHeaders')}
</h4>
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(request.requestHeaders))}
className='hover:bg-primary hover:text-primary-foreground'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(request.requestHeaders), `request-headers-${request.id}.json`)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
<div className='bg-muted/20 h-[300px] w-full overflow-auto rounded-lg border p-4'>
<JsonViewer data={request.requestHeaders} rootName='' defaultExpanded={true} className='text-sm' />
</div>
</div>
)}
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<h4 className='flex items-center gap-2 text-base font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.requestBody')}
</h4>
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(request.requestBody))}
className='hover:bg-primary hover:text-primary-foreground'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(request.requestBody), `request-body-${request.id}.json`)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
<div className='bg-muted/20 h-[500px] w-full overflow-auto rounded-lg border p-4'>
<JsonViewer data={request.requestBody} rootName='' defaultExpanded={true} className='text-sm' />
</div>
</div>
</TabsContent>
<TabsContent value='response' className='space-y-6 p-6'>
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<h4 className='flex items-center gap-2 text-base font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.responseBody')}
</h4>
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={showResponseChunksModal}
disabled={!request?.responseChunks || request.responseChunks.length === 0}
className='hover:bg-primary hover:text-primary-foreground disabled:opacity-50'
>
<Layers className='mr-2 h-4 w-4' />
{t('requests.columns.responseChunks')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(request.responseBody))}
disabled={!request.responseBody}
className='hover:bg-primary hover:text-primary-foreground disabled:opacity-50'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(request.responseBody), `response-body-${request.id}.json`)}
disabled={!request.responseBody}
className='hover:bg-primary hover:text-primary-foreground disabled:opacity-50'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
{request.responseBody ? (
<div className='bg-muted/20 h-[500px] w-full overflow-auto rounded-lg border p-4'>
<JsonViewer data={request.responseBody} rootName='' defaultExpanded={true} className='text-sm' />
</div>
) : (
<div className='bg-muted/20 flex h-[500px] w-full items-center justify-center rounded-lg border'>
<div className='space-y-3 text-center'>
<FileText className='text-muted-foreground mx-auto h-12 w-12' />
<p className='text-muted-foreground text-base'>{t('requests.detail.noResponse')}</p>
</div>
</div>
)}
</div>
</TabsContent>
<TabsContent value='executions' className='space-y-6 p-6'>
{executions && executions.edges.length > 0 ? (
<div className='space-y-6'>
{executions.edges.map((edge: any, index: number) => {
const execution = edge.node;
return (
<Card key={execution.id} className='bg-muted/20 border-0 shadow-sm'>
<CardHeader className='pb-4'>
<div className='flex items-center justify-between'>
<h5 className='flex items-center gap-2 text-base font-semibold'>
<div className='bg-primary/10 text-primary flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold'>
{index + 1}
</div>
{t('requests.dialogs.requestDetail.execution', { index: index + 1 })}
</h5>
<Badge className={getStatusColor(execution.status)} variant='secondary'>
{t(`requests.status.${execution.status}`)}
</Badge>
</div>
</CardHeader>
<CardContent className='space-y-6'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-5'>
<div className='bg-background space-y-2 rounded-lg border p-3'>
<span className='flex items-center gap-2 text-sm font-medium'>
<Database className='text-primary h-4 w-4' />
{t('requests.columns.channel')}
</span>
<p className='text-muted-foreground font-mono text-sm'>
{execution.channel?.name || t('requests.columns.unknown')}
</p>
</div>
<div className='bg-background space-y-2 rounded-lg border p-3'>
<span className='flex items-center gap-2 text-sm font-medium'>
<Clock className='text-primary h-4 w-4' />
{t('requests.dialogs.requestDetail.fields.startTime')}
</span>
<p className='text-muted-foreground font-mono text-sm'>
{execution.createdAt
? format(new Date(execution.createdAt), 'yyyy-MM-dd HH:mm:ss', { locale })
: t('requests.columns.unknown')}
</p>
</div>
<div className='bg-background space-y-2 rounded-lg border p-3'>
<span className='flex items-center gap-2 text-sm font-medium'>
<Clock className='text-primary h-4 w-4' />
{t('requests.dialogs.requestDetail.fields.endTime')}
</span>
<p className='text-muted-foreground font-mono text-sm'>
{execution.status === 'completed' || execution.status === 'failed'
? execution.updatedAt
? format(new Date(execution.updatedAt), 'yyyy-MM-dd HH:mm:ss', { locale })
: t('requests.columns.unknown')
: '-'}
</p>
</div>
<div className='bg-background space-y-2 rounded-lg border p-3'>
<span className='flex items-center gap-2 text-sm font-medium'>
<Clock className='text-primary h-4 w-4' />
{t('requests.columns.latency')}
</span>
<p className='text-muted-foreground font-mono text-sm'>
{execution.status === 'completed' || execution.status === 'failed'
? formatLatency(calculateLatency(execution.createdAt, execution.updatedAt))
: '-'}
</p>
</div>
<div className='bg-background space-y-2 rounded-lg border p-3'>
<span className='flex items-center gap-2 text-sm font-medium'>
<Clock className='text-primary h-4 w-4' />
{t('requests.columns.firstTokenLatency')}
</span>
<p className='text-muted-foreground font-mono text-sm'>
{execution.status === 'completed' && execution.metricsFirstTokenLatencyMs != null
? formatLatency(execution.metricsFirstTokenLatencyMs)
: '-'}
</p>
</div>
</div>
{execution.errorMessage && (
<div className='bg-destructive/5 border-destructive/20 space-y-3 rounded-lg border p-4'>
<span className='text-destructive flex items-center gap-2 text-sm font-semibold'>
<FileText className='h-4 w-4' />
{t('common.messages.errorMessage')}
</span>
<p className='text-destructive bg-destructive/10 rounded border p-3 text-sm'>{execution.errorMessage}</p>
</div>
)}
{(execution.requestHeaders || execution.requestBody) && (
<div className='flex justify-end'>
<Button
variant='outline'
size='sm'
onClick={() => showExecutionCurlPreview(execution.requestHeaders, execution.requestBody, execution.channel, execution.format)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Terminal className='mr-2 h-4 w-4' />
{t('requests.actions.copyCurl')}
</Button>
</div>
)}
{execution.requestHeaders && (
<div className='space-y-3'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-2 text-sm font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.requestHeaders')}
</span>
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(execution.requestHeaders))}
className='hover:bg-primary hover:text-primary-foreground'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(execution.requestHeaders), `execution-${execution.id}-request-headers.json`)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
<div className='bg-background h-64 w-full overflow-auto rounded-lg border p-3'>
<JsonViewer data={execution.requestHeaders} rootName='' defaultExpanded={false} className='text-xs' />
</div>
</div>
)}
{execution.requestBody && (
<div className='space-y-3'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-2 text-sm font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.requestBody')}
</span>
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(execution.requestBody))}
className='hover:bg-primary hover:text-primary-foreground'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(execution.requestBody), `execution-${execution.id}-request-body.json`)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
<div className='bg-background h-64 w-full overflow-auto rounded-lg border p-3'>
<JsonViewer data={execution.requestBody} rootName='' defaultExpanded={false} className='text-xs' />
</div>
</div>
)}
{execution.responseBody && (
<div className='space-y-3'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-2 text-sm font-semibold'>
<FileText className='text-primary h-4 w-4' />
{t('requests.columns.responseBody')}
</span>
<div className='flex gap-2'>
{execution.responseChunks && execution.responseChunks.length > 0 && (
<Button
variant='outline'
size='sm'
onClick={() => showExecutionChunksModal(execution.responseChunks)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Layers className='mr-2 h-4 w-4' />
{t('requests.columns.responseChunks')}
</Button>
)}
<Button
variant='outline'
size='sm'
onClick={() => copyToClipboard(formatJson(execution.responseBody))}
className='hover:bg-primary hover:text-primary-foreground'
>
<Copy className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.copy')}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => downloadFile(formatJson(execution.responseBody), `execution-${execution.id}-response-body.json`)}
className='hover:bg-primary hover:text-primary-foreground'
>
<Download className='mr-2 h-4 w-4' />
{t('requests.dialogs.jsonViewer.download')}
</Button>
</div>
</div>
<div className='bg-background h-64 w-full overflow-auto rounded-lg border p-3'>
<JsonViewer data={execution.responseBody} rootName='' defaultExpanded={false} className='text-xs' />
</div>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
) : (
<div className='py-16 text-center'>
<div className='space-y-4'>
<FileText className='text-muted-foreground mx-auto h-16 w-16' />
<p className='text-muted-foreground text-lg'>{t('requests.dialogs.requestDetail.noExecutions')}</p>
</div>
</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</Main>
{/* Response Chunks Modal */}
<ChunksDialog
open={showResponseChunks}
onOpenChange={setShowResponseChunks}
chunks={selectedResponseChunks}
title={t('requests.dialogs.jsonViewer.responseChunks')}
/>
{/* Execution Chunks Modal */}
<ChunksDialog
open={showExecutionChunks}
onOpenChange={setShowExecutionChunks}
chunks={selectedExecutionChunks}
title={t('requests.dialogs.jsonViewer.responseChunks')}
/>
{/* cURL Preview Modal */}
<CurlPreviewDialog
open={showCurlPreview}
onOpenChange={setShowCurlPreview}
curlCommand={curlCommand}
/>
</div>
);
}