158 lines
5.9 KiB
TypeScript
158 lines
5.9 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { aiAssistantApi } from "@/lib/api/aiAssistant";
|
||
|
|
import type { UsageLogItem } from "@/lib/api/aiAssistant";
|
||
|
|
import { History, Loader2, MessageSquare, Clock, Zap, CheckCircle, XCircle } from "lucide-react";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from "@/components/ui/table";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
export default function AiAssistantHistoryPage() {
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [logs, setLogs] = useState<UsageLogItem[]>([]);
|
||
|
|
const [page, setPage] = useState(1);
|
||
|
|
const [totalPages, setTotalPages] = useState(1);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadLogs();
|
||
|
|
}, [page]);
|
||
|
|
|
||
|
|
const loadLogs = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const res = await aiAssistantApi.get(`/usage/logs?page=${page}&limit=20`);
|
||
|
|
const data = res.data?.data as { logs?: UsageLogItem[]; pagination?: { totalPages?: number } };
|
||
|
|
setLogs(data?.logs ?? []);
|
||
|
|
setTotalPages(data?.pagination?.totalPages ?? 1);
|
||
|
|
} catch {
|
||
|
|
toast.error("대화 이력을 불러오는데 실패했습니다.");
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading && logs.length === 0) {
|
||
|
|
return (
|
||
|
|
<div className="flex h-64 items-center justify-center">
|
||
|
|
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">대화 이력</h1>
|
||
|
|
<p className="text-muted-foreground mt-1">AI Assistant와의 대화 기록을 확인합니다.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>API 호출 로그</CardTitle>
|
||
|
|
<CardDescription>최근 API 호출 기록</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{logs.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||
|
|
<History className="text-muted-foreground mb-4 h-12 w-12" />
|
||
|
|
<h3 className="text-lg font-medium">대화 이력이 없습니다</h3>
|
||
|
|
<p className="text-muted-foreground mt-1 text-sm">AI 채팅을 시작하면 이력이 표시됩니다.</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead>상태</TableHead>
|
||
|
|
<TableHead>프로바이더</TableHead>
|
||
|
|
<TableHead>모델</TableHead>
|
||
|
|
<TableHead>토큰</TableHead>
|
||
|
|
<TableHead>비용</TableHead>
|
||
|
|
<TableHead>응답시간</TableHead>
|
||
|
|
<TableHead>일시</TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{logs.map((log) => (
|
||
|
|
<TableRow key={log.id}>
|
||
|
|
<TableCell>
|
||
|
|
{log.success ? (
|
||
|
|
<Badge variant="success" className="gap-1">
|
||
|
|
<CheckCircle className="h-3 w-3" />
|
||
|
|
성공
|
||
|
|
</Badge>
|
||
|
|
) : (
|
||
|
|
<Badge variant="destructive" className="gap-1">
|
||
|
|
<XCircle className="h-3 w-3" />
|
||
|
|
실패
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<Badge variant="outline">{log.providerName}</Badge>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell className="font-mono text-sm">{log.modelName}</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-1">
|
||
|
|
<Zap className="text-muted-foreground h-3 w-3" />
|
||
|
|
<span>{(log.totalTokens ?? 0).toLocaleString()}</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-muted-foreground text-xs">
|
||
|
|
입력: {log.promptTokens ?? 0} / 출력: {log.completionTokens ?? 0}
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>${(log.costUsd ?? 0).toFixed(6)}</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-1">
|
||
|
|
<Clock className="text-muted-foreground h-3 w-3" />
|
||
|
|
<span>{log.responseTimeMs ?? 0}ms</span>
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell className="text-muted-foreground">
|
||
|
|
{new Date(log.createdAt).toLocaleString("ko-KR")}
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
{totalPages > 1 && (
|
||
|
|
<div className="mt-4 flex items-center justify-center gap-2">
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||
|
|
disabled={page === 1}
|
||
|
|
>
|
||
|
|
이전
|
||
|
|
</Button>
|
||
|
|
<span className="text-muted-foreground text-sm">
|
||
|
|
{page} / {totalPages}
|
||
|
|
</span>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||
|
|
disabled={page === totalPages}
|
||
|
|
>
|
||
|
|
다음
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|