|
@@ -1,13 +1,22 @@
|
|
|
"use client";
|
|
"use client";
|
|
|
|
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
-import { ArrowLeft, Check, Copy, Download, Hash, Monitor, XCircle } from "lucide-react";
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ ArrowLeft,
|
|
|
|
|
+ Check,
|
|
|
|
|
+ Copy,
|
|
|
|
|
+ Download,
|
|
|
|
|
+ Info,
|
|
|
|
|
+ Menu,
|
|
|
|
|
+ Monitor,
|
|
|
|
|
+ MoreVertical,
|
|
|
|
|
+ XCircle,
|
|
|
|
|
+} from "lucide-react";
|
|
|
import { useParams, useSearchParams } from "next/navigation";
|
|
import { useParams, useSearchParams } from "next/navigation";
|
|
|
import { useTranslations } from "next-intl";
|
|
import { useTranslations } from "next-intl";
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
import { toast } from "sonner";
|
|
import { toast } from "sonner";
|
|
|
import { getSessionDetails, terminateActiveSession } from "@/actions/active-sessions";
|
|
import { getSessionDetails, terminateActiveSession } from "@/actions/active-sessions";
|
|
|
-import { Section } from "@/components/section";
|
|
|
|
|
import {
|
|
import {
|
|
|
AlertDialog,
|
|
AlertDialog,
|
|
|
AlertDialogAction,
|
|
AlertDialogAction,
|
|
@@ -20,12 +29,20 @@ import {
|
|
|
} from "@/components/ui/alert-dialog";
|
|
} from "@/components/ui/alert-dialog";
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Button } from "@/components/ui/button";
|
|
|
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ DropdownMenu,
|
|
|
|
|
+ DropdownMenuContent,
|
|
|
|
|
+ DropdownMenuItem,
|
|
|
|
|
+ DropdownMenuTrigger,
|
|
|
|
|
+} from "@/components/ui/dropdown-menu";
|
|
|
|
|
+import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
|
|
|
|
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
|
import { usePathname, useRouter } from "@/i18n/routing";
|
|
import { usePathname, useRouter } from "@/i18n/routing";
|
|
|
-import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
|
|
|
|
|
|
|
+import type { CurrencyCode } from "@/lib/utils/currency";
|
|
|
import { RequestListSidebar } from "./request-list-sidebar";
|
|
import { RequestListSidebar } from "./request-list-sidebar";
|
|
|
import { type SessionMessages, SessionMessagesDetailsTabs } from "./session-details-tabs";
|
|
import { type SessionMessages, SessionMessagesDetailsTabs } from "./session-details-tabs";
|
|
|
import { isSessionMessages } from "./session-messages-guards";
|
|
import { isSessionMessages } from "./session-messages-guards";
|
|
|
|
|
+import { SessionStats } from "./session-stats";
|
|
|
|
|
|
|
|
async function fetchSystemSettings(): Promise<{
|
|
async function fetchSystemSettings(): Promise<{
|
|
|
currencyDisplay: CurrencyCode;
|
|
currencyDisplay: CurrencyCode;
|
|
@@ -39,14 +56,14 @@ async function fetchSystemSettings(): Promise<{
|
|
|
|
|
|
|
|
export function SessionMessagesClient() {
|
|
export function SessionMessagesClient() {
|
|
|
const t = useTranslations("dashboard.sessions");
|
|
const t = useTranslations("dashboard.sessions");
|
|
|
- const tDesc = useTranslations("dashboard.description");
|
|
|
|
|
|
|
+
|
|
|
const params = useParams();
|
|
const params = useParams();
|
|
|
const searchParams = useSearchParams();
|
|
const searchParams = useSearchParams();
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const pathname = usePathname();
|
|
const pathname = usePathname();
|
|
|
const sessionId = params.sessionId as string;
|
|
const sessionId = params.sessionId as string;
|
|
|
|
|
|
|
|
- // 从 URL 获取当前选中的请求序号
|
|
|
|
|
|
|
+ // URL state
|
|
|
const seqParam = searchParams.get("seq");
|
|
const seqParam = searchParams.get("seq");
|
|
|
const selectedSeq = (() => {
|
|
const selectedSeq = (() => {
|
|
|
if (!seqParam) return null;
|
|
if (!seqParam) return null;
|
|
@@ -55,6 +72,7 @@ export function SessionMessagesClient() {
|
|
|
return parsed;
|
|
return parsed;
|
|
|
})();
|
|
})();
|
|
|
|
|
|
|
|
|
|
+ // Data State
|
|
|
const [messages, setMessages] = useState<SessionMessages | null>(null);
|
|
const [messages, setMessages] = useState<SessionMessages | null>(null);
|
|
|
const [requestBody, setRequestBody] = useState<unknown | null>(null);
|
|
const [requestBody, setRequestBody] = useState<unknown | null>(null);
|
|
|
const [response, setResponse] = useState<string | null>(null);
|
|
const [response, setResponse] = useState<string | null>(null);
|
|
@@ -83,6 +101,8 @@ export function SessionMessagesClient() {
|
|
|
const [currentSequence, setCurrentSequence] = useState<number | null>(null);
|
|
const [currentSequence, setCurrentSequence] = useState<number | null>(null);
|
|
|
const [prevSequence, setPrevSequence] = useState<number | null>(null);
|
|
const [prevSequence, setPrevSequence] = useState<number | null>(null);
|
|
|
const [nextSequence, setNextSequence] = useState<number | null>(null);
|
|
const [nextSequence, setNextSequence] = useState<number | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ // UI State
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [copiedRequest, setCopiedRequest] = useState(false);
|
|
const [copiedRequest, setCopiedRequest] = useState(false);
|
|
@@ -90,6 +110,8 @@ export function SessionMessagesClient() {
|
|
|
const [showTerminateDialog, setShowTerminateDialog] = useState(false);
|
|
const [showTerminateDialog, setShowTerminateDialog] = useState(false);
|
|
|
const [isTerminating, setIsTerminating] = useState(false);
|
|
const [isTerminating, setIsTerminating] = useState(false);
|
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
|
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
|
|
|
+ const [isMobileStatsOpen, setIsMobileStatsOpen] = useState(false);
|
|
|
|
|
|
|
|
const resetDetailsState = useCallback(() => {
|
|
const resetDetailsState = useCallback(() => {
|
|
|
setMessages(null);
|
|
setMessages(null);
|
|
@@ -113,12 +135,12 @@ export function SessionMessagesClient() {
|
|
|
|
|
|
|
|
const currencyCode = systemSettings?.currencyDisplay || "USD";
|
|
const currencyCode = systemSettings?.currencyDisplay || "USD";
|
|
|
|
|
|
|
|
- // 处理请求选择(更新 URL)
|
|
|
|
|
const handleSelectRequest = useCallback(
|
|
const handleSelectRequest = useCallback(
|
|
|
(seq: number) => {
|
|
(seq: number) => {
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
params.set("seq", seq.toString());
|
|
params.set("seq", seq.toString());
|
|
|
router.replace(`${pathname}?${params.toString()}`);
|
|
router.replace(`${pathname}?${params.toString()}`);
|
|
|
|
|
+ setIsMobileMenuOpen(false);
|
|
|
},
|
|
},
|
|
|
[router, pathname]
|
|
[router, pathname]
|
|
|
);
|
|
);
|
|
@@ -131,7 +153,6 @@ export function SessionMessagesClient() {
|
|
|
setError(null);
|
|
setError(null);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 传入 requestSequence 参数以获取特定请求的消息
|
|
|
|
|
const result = await getSessionDetails(sessionId, selectedSeq ?? undefined);
|
|
const result = await getSessionDetails(sessionId, selectedSeq ?? undefined);
|
|
|
if (cancelled) return;
|
|
if (cancelled) return;
|
|
|
|
|
|
|
@@ -174,6 +195,7 @@ export function SessionMessagesClient() {
|
|
|
const canExportRequest =
|
|
const canExportRequest =
|
|
|
!isLoading && error === null && requestHeaders !== null && requestBody !== null;
|
|
!isLoading && error === null && requestHeaders !== null && requestBody !== null;
|
|
|
const exportSequence = selectedSeq ?? currentSequence;
|
|
const exportSequence = selectedSeq ?? currentSequence;
|
|
|
|
|
+
|
|
|
const getRequestExportJson = () => {
|
|
const getRequestExportJson = () => {
|
|
|
return JSON.stringify(
|
|
return JSON.stringify(
|
|
|
{
|
|
{
|
|
@@ -191,31 +213,32 @@ export function SessionMessagesClient() {
|
|
|
|
|
|
|
|
const handleCopyRequest = async () => {
|
|
const handleCopyRequest = async () => {
|
|
|
if (!canExportRequest) return;
|
|
if (!canExportRequest) return;
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
await navigator.clipboard.writeText(getRequestExportJson());
|
|
await navigator.clipboard.writeText(getRequestExportJson());
|
|
|
setCopiedRequest(true);
|
|
setCopiedRequest(true);
|
|
|
setTimeout(() => setCopiedRequest(false), 2000);
|
|
setTimeout(() => setCopiedRequest(false), 2000);
|
|
|
|
|
+ toast.success(t("actions.copied"));
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error(t("errors.copyFailed"), err);
|
|
console.error(t("errors.copyFailed"), err);
|
|
|
|
|
+ toast.error(t("errors.copyFailed"));
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleCopyResponse = async () => {
|
|
const handleCopyResponse = async () => {
|
|
|
if (!response) return;
|
|
if (!response) return;
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
await navigator.clipboard.writeText(response);
|
|
await navigator.clipboard.writeText(response);
|
|
|
setCopiedResponse(true);
|
|
setCopiedResponse(true);
|
|
|
setTimeout(() => setCopiedResponse(false), 2000);
|
|
setTimeout(() => setCopiedResponse(false), 2000);
|
|
|
|
|
+ toast.success(t("actions.copied"));
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error(t("errors.copyFailed"), err);
|
|
console.error(t("errors.copyFailed"), err);
|
|
|
|
|
+ toast.error(t("errors.copyFailed"));
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleDownloadRequest = () => {
|
|
const handleDownloadRequest = () => {
|
|
|
if (!canExportRequest) return;
|
|
if (!canExportRequest) return;
|
|
|
-
|
|
|
|
|
const jsonStr = getRequestExportJson();
|
|
const jsonStr = getRequestExportJson();
|
|
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
|
const url = URL.createObjectURL(blob);
|
|
const url = URL.createObjectURL(blob);
|
|
@@ -235,7 +258,6 @@ export function SessionMessagesClient() {
|
|
|
const result = await terminateActiveSession(sessionId);
|
|
const result = await terminateActiveSession(sessionId);
|
|
|
if (result.ok) {
|
|
if (result.ok) {
|
|
|
toast.success(t("actions.terminateSuccess"));
|
|
toast.success(t("actions.terminateSuccess"));
|
|
|
- // 终止成功后返回列表页
|
|
|
|
|
router.push("/dashboard/sessions");
|
|
router.push("/dashboard/sessions");
|
|
|
} else {
|
|
} else {
|
|
|
toast.error(result.error || t("actions.terminateFailed"));
|
|
toast.error(result.error || t("actions.terminateFailed"));
|
|
@@ -248,138 +270,246 @@ export function SessionMessagesClient() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 计算总 Token(从聚合统计)
|
|
|
|
|
- const totalTokens =
|
|
|
|
|
- (sessionStats?.totalInputTokens || 0) +
|
|
|
|
|
- (sessionStats?.totalOutputTokens || 0) +
|
|
|
|
|
- (sessionStats?.totalCacheCreationTokens || 0) +
|
|
|
|
|
- (sessionStats?.totalCacheReadTokens || 0);
|
|
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div className="flex h-full">
|
|
|
|
|
- {/* 左侧:请求列表侧边栏 */}
|
|
|
|
|
- <RequestListSidebar
|
|
|
|
|
- sessionId={sessionId}
|
|
|
|
|
- selectedSeq={selectedSeq ?? currentSequence}
|
|
|
|
|
- onSelect={handleSelectRequest}
|
|
|
|
|
- collapsed={sidebarCollapsed}
|
|
|
|
|
- onCollapsedChange={setSidebarCollapsed}
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- {/* 主内容区域 */}
|
|
|
|
|
- <div className="flex-1 overflow-auto">
|
|
|
|
|
- <div className="space-y-6 p-6">
|
|
|
|
|
- {/* 标题栏 */}
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <div className="flex items-center gap-4">
|
|
|
|
|
- <Button variant="outline" size="sm" onClick={() => router.back()}>
|
|
|
|
|
- <ArrowLeft className="h-4 w-4 mr-2" />
|
|
|
|
|
|
|
+ <div className="flex h-full bg-background">
|
|
|
|
|
+ {/* Mobile Sidebar (Requests) */}
|
|
|
|
|
+ <Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
|
|
|
|
|
+ <SheetContent side="left" className="p-0 w-[300px]">
|
|
|
|
|
+ <SheetHeader className="p-4 border-b">
|
|
|
|
|
+ <SheetTitle>{t("requestList.title")}</SheetTitle>
|
|
|
|
|
+ </SheetHeader>
|
|
|
|
|
+ <div className="h-full">
|
|
|
|
|
+ <RequestListSidebar
|
|
|
|
|
+ sessionId={sessionId}
|
|
|
|
|
+ selectedSeq={selectedSeq ?? currentSequence}
|
|
|
|
|
+ onSelect={handleSelectRequest}
|
|
|
|
|
+ className="border-none w-full"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SheetContent>
|
|
|
|
|
+ </Sheet>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Mobile Stats (Right Sheet) */}
|
|
|
|
|
+ {sessionStats && (
|
|
|
|
|
+ <Sheet open={isMobileStatsOpen} onOpenChange={setIsMobileStatsOpen}>
|
|
|
|
|
+ <SheetContent side="right" className="w-[300px] overflow-y-auto">
|
|
|
|
|
+ <SheetHeader className="pb-4">
|
|
|
|
|
+ <SheetTitle>{t("details.overview")}</SheetTitle>
|
|
|
|
|
+ </SheetHeader>
|
|
|
|
|
+ <SessionStats stats={sessionStats} currencyCode={currencyCode} />
|
|
|
|
|
+ </SheetContent>
|
|
|
|
|
+ </Sheet>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Desktop Left Sidebar (Requests) */}
|
|
|
|
|
+ <aside className="hidden md:flex flex-col border-r bg-muted/10 h-full transition-all duration-300 ease-in-out relative group">
|
|
|
|
|
+ <div className={sidebarCollapsed ? "w-16" : "w-72"}>
|
|
|
|
|
+ <RequestListSidebar
|
|
|
|
|
+ sessionId={sessionId}
|
|
|
|
|
+ selectedSeq={selectedSeq ?? currentSequence}
|
|
|
|
|
+ onSelect={handleSelectRequest}
|
|
|
|
|
+ collapsed={sidebarCollapsed}
|
|
|
|
|
+ className="h-full"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
|
|
|
+ className="absolute -right-3 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full border bg-background shadow-md opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
|
|
|
+ >
|
|
|
|
|
+ <MoreVertical className="h-3 w-3" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Main Content Area */}
|
|
|
|
|
+ <main className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
|
|
|
|
|
+ {/* Header */}
|
|
|
|
|
+ <header className="flex-none h-16 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-6 flex items-center justify-between z-10">
|
|
|
|
|
+ <div className="flex items-center gap-4 min-w-0">
|
|
|
|
|
+ {/* Mobile Menu Toggle */}
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ className="md:hidden"
|
|
|
|
|
+ onClick={() => setIsMobileMenuOpen(true)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Menu className="h-5 w-5" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ className="h-8 -ml-2 text-muted-foreground"
|
|
|
|
|
+ onClick={() => router.back()}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ArrowLeft className="h-4 w-4 mr-1" />
|
|
|
{t("actions.back")}
|
|
{t("actions.back")}
|
|
|
</Button>
|
|
</Button>
|
|
|
- <div>
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <h1 className="text-2xl font-bold">{t("details.title")}</h1>
|
|
|
|
|
- {(selectedSeq ?? currentSequence) && (
|
|
|
|
|
- <Badge variant="outline" className="text-sm">
|
|
|
|
|
- #{selectedSeq ?? currentSequence}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-sm text-muted-foreground font-mono mt-1">{sessionId}</p>
|
|
|
|
|
|
|
+ <span className="text-muted-foreground/40">/</span>
|
|
|
|
|
+ <div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
+ <h1 className="font-semibold text-foreground truncate">{t("details.title")}</h1>
|
|
|
|
|
+ <Badge
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ className="font-mono font-normal text-xs bg-muted/50 truncate max-w-[100px] sm:max-w-none"
|
|
|
|
|
+ >
|
|
|
|
|
+ {sessionId}
|
|
|
|
|
+ </Badge>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* 操作按钮 */}
|
|
|
|
|
- <div className="flex gap-2">
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ {/* Desktop Actions */}
|
|
|
|
|
+ <div className="hidden sm:flex items-center gap-2">
|
|
|
{canExportRequest && (
|
|
{canExportRequest && (
|
|
|
<>
|
|
<>
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={handleCopyRequest}
|
|
|
|
|
- disabled={copiedRequest}
|
|
|
|
|
- >
|
|
|
|
|
- {copiedRequest ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Check className="h-4 w-4 mr-2" />
|
|
|
|
|
- {t("actions.copied")}
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <>
|
|
|
|
|
- <Copy className="h-4 w-4 mr-2" />
|
|
|
|
|
- {t("actions.copyMessages")}
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" size="sm" onClick={handleDownloadRequest}>
|
|
|
|
|
- <Download className="h-4 w-4 mr-2" />
|
|
|
|
|
- {t("actions.downloadMessages")}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <TooltipProvider>
|
|
|
|
|
+ <Tooltip>
|
|
|
|
|
+ <TooltipTrigger asChild>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ className="h-8 w-8"
|
|
|
|
|
+ onClick={handleCopyRequest}
|
|
|
|
|
+ >
|
|
|
|
|
+ {copiedRequest ? (
|
|
|
|
|
+ <Check className="h-4 w-4 text-green-500" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Copy className="h-4 w-4" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </TooltipTrigger>
|
|
|
|
|
+ <TooltipContent>{t("actions.copyMessages")}</TooltipContent>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ </TooltipProvider>
|
|
|
|
|
+
|
|
|
|
|
+ <TooltipProvider>
|
|
|
|
|
+ <Tooltip>
|
|
|
|
|
+ <TooltipTrigger asChild>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ className="h-8 w-8"
|
|
|
|
|
+ onClick={handleDownloadRequest}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Download className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </TooltipTrigger>
|
|
|
|
|
+ <TooltipContent>{t("actions.downloadMessages")}</TooltipContent>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ </TooltipProvider>
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
- {/* 终止 Session 按钮 */}
|
|
|
|
|
|
|
+
|
|
|
{sessionStats && (
|
|
{sessionStats && (
|
|
|
<Button
|
|
<Button
|
|
|
variant="destructive"
|
|
variant="destructive"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
|
|
+ className="h-8"
|
|
|
onClick={() => setShowTerminateDialog(true)}
|
|
onClick={() => setShowTerminateDialog(true)}
|
|
|
- disabled={isTerminating}
|
|
|
|
|
>
|
|
>
|
|
|
<XCircle className="h-4 w-4 mr-2" />
|
|
<XCircle className="h-4 w-4 mr-2" />
|
|
|
{t("actions.terminate")}
|
|
{t("actions.terminate")}
|
|
|
</Button>
|
|
</Button>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- {/* 内容区域 */}
|
|
|
|
|
- {isLoading ? (
|
|
|
|
|
- <div className="text-center py-16 text-muted-foreground">{t("status.loading")}</div>
|
|
|
|
|
- ) : error ? (
|
|
|
|
|
- <div className="text-center py-16">
|
|
|
|
|
- <div className="text-destructive text-lg mb-2">{error}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
|
- {/* 左侧:完整内容(占 2 列)*/}
|
|
|
|
|
- <div className="lg:col-span-2 space-y-6">
|
|
|
|
|
- {/* User-Agent 信息 */}
|
|
|
|
|
- {sessionStats?.userAgent && (
|
|
|
|
|
- <Section title={t("details.clientInfo")} description={tDesc("clientInfo")}>
|
|
|
|
|
- <div className="rounded-md border bg-muted/50 p-4">
|
|
|
|
|
- <div className="flex items-start gap-3">
|
|
|
|
|
- <Monitor className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
|
|
|
- <code className="text-sm font-mono break-all">
|
|
|
|
|
- {sessionStats.userAgent}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </Section>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {/* Mobile Actions Dropdown */}
|
|
|
|
|
+ <div className="sm:hidden flex items-center gap-2">
|
|
|
|
|
+ {/* Info Toggle for Mobile */}
|
|
|
|
|
+ {sessionStats && (
|
|
|
|
|
+ <Button variant="ghost" size="icon" onClick={() => setIsMobileStatsOpen(true)}>
|
|
|
|
|
+ <Info className="h-5 w-5" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- {response !== null && (
|
|
|
|
|
- <div className="flex justify-end">
|
|
|
|
|
|
|
+ <DropdownMenu>
|
|
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
|
|
+ <Button variant="ghost" size="icon">
|
|
|
|
|
+ <MoreVertical className="h-5 w-5" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DropdownMenuTrigger>
|
|
|
|
|
+ <DropdownMenuContent align="end">
|
|
|
|
|
+ {canExportRequest && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <DropdownMenuItem onClick={handleCopyRequest}>
|
|
|
|
|
+ <Copy className="h-4 w-4 mr-2" /> {t("actions.copyMessages")}
|
|
|
|
|
+ </DropdownMenuItem>
|
|
|
|
|
+ <DropdownMenuItem onClick={handleDownloadRequest}>
|
|
|
|
|
+ <Download className="h-4 w-4 mr-2" /> {t("actions.downloadMessages")}
|
|
|
|
|
+ </DropdownMenuItem>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {sessionStats && (
|
|
|
|
|
+ <DropdownMenuItem
|
|
|
|
|
+ className="text-destructive focus:text-destructive"
|
|
|
|
|
+ onClick={() => setShowTerminateDialog(true)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <XCircle className="h-4 w-4 mr-2" /> {t("actions.terminate")}
|
|
|
|
|
+ </DropdownMenuItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </DropdownMenuContent>
|
|
|
|
|
+ </DropdownMenu>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 3-Column Content Layout */}
|
|
|
|
|
+ <div className="flex-1 flex overflow-hidden">
|
|
|
|
|
+ {/* Center: Scrollable Content */}
|
|
|
|
|
+ <div className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
|
|
|
|
|
+ <div className="max-w-5xl mx-auto space-y-6">
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ <div className="flex flex-col items-center justify-center py-32 text-muted-foreground animate-pulse">
|
|
|
|
|
+ <div className="h-8 w-8 border-2 border-primary border-t-transparent rounded-full animate-spin mb-4" />
|
|
|
|
|
+ <p>{t("status.loading")}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : error ? (
|
|
|
|
|
+ <div className="rounded-lg border border-destructive/20 bg-destructive/5 p-8 text-center">
|
|
|
|
|
+ <XCircle className="h-8 w-8 text-destructive mx-auto mb-4" />
|
|
|
|
|
+ <h3 className="text-lg font-semibold text-destructive">{t("status.error")}</h3>
|
|
|
|
|
+ <p className="text-muted-foreground mt-2">{error}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* Nav & Info Banner */}
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ disabled={!prevSequence}
|
|
|
|
|
+ onClick={() => prevSequence && handleSelectRequest(prevSequence)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ArrowLeft className="h-4 w-4 mr-2" />
|
|
|
|
|
+ {t("details.prevRequest")}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Badge variant="secondary">#{selectedSeq ?? currentSequence ?? "-"}</Badge>
|
|
|
<Button
|
|
<Button
|
|
|
- variant="ghost"
|
|
|
|
|
|
|
+ variant="outline"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
- onClick={handleCopyResponse}
|
|
|
|
|
- disabled={copiedResponse}
|
|
|
|
|
|
|
+ disabled={!nextSequence}
|
|
|
|
|
+ onClick={() => nextSequence && handleSelectRequest(nextSequence)}
|
|
|
|
|
+ className="flex-row-reverse"
|
|
|
>
|
|
>
|
|
|
- {copiedResponse ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Check className="h-4 w-4 mr-2" />
|
|
|
|
|
- {t("actions.copied")}
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <>
|
|
|
|
|
- <Copy className="h-4 w-4 mr-2" />
|
|
|
|
|
- {t("actions.copyResponse")}
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <ArrowLeft className="h-4 w-4 ml-2 rotate-180" />
|
|
|
|
|
+ {t("details.nextRequest")}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {sessionStats?.userAgent && (
|
|
|
|
|
+ <div className="bg-muted/30 rounded-lg p-3 flex items-start gap-3 border text-sm text-muted-foreground">
|
|
|
|
|
+ <Monitor className="h-4 w-4 mt-0.5 text-blue-500 shrink-0" />
|
|
|
|
|
+ <code className="break-all font-mono text-xs">
|
|
|
|
|
+ {sessionStats.userAgent}
|
|
|
|
|
+ </code>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Main Content - No more extra Card wrapper */}
|
|
|
<SessionMessagesDetailsTabs
|
|
<SessionMessagesDetailsTabs
|
|
|
messages={messages}
|
|
messages={messages}
|
|
|
requestBody={requestBody}
|
|
requestBody={requestBody}
|
|
@@ -389,264 +519,50 @@ export function SessionMessagesClient() {
|
|
|
responseHeaders={responseHeaders}
|
|
responseHeaders={responseHeaders}
|
|
|
requestMeta={requestMeta}
|
|
requestMeta={requestMeta}
|
|
|
responseMeta={responseMeta}
|
|
responseMeta={responseMeta}
|
|
|
|
|
+ onCopyResponse={handleCopyResponse}
|
|
|
|
|
+ isResponseCopied={copiedResponse}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- disabled={!prevSequence}
|
|
|
|
|
- onClick={() => prevSequence && handleSelectRequest(prevSequence)}
|
|
|
|
|
- >
|
|
|
|
|
- {t("details.prevRequest")}
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- disabled={!nextSequence}
|
|
|
|
|
- onClick={() => nextSequence && handleSelectRequest(nextSequence)}
|
|
|
|
|
- >
|
|
|
|
|
- {t("details.nextRequest")}
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 无数据提示 */}
|
|
|
|
|
- {!sessionStats?.userAgent &&
|
|
|
|
|
- !messages &&
|
|
|
|
|
- !requestBody &&
|
|
|
|
|
- !response &&
|
|
|
|
|
- !requestHeaders &&
|
|
|
|
|
- !responseHeaders && (
|
|
|
|
|
- <div className="text-center py-16">
|
|
|
|
|
- <div className="text-muted-foreground text-lg mb-2">
|
|
|
|
|
- {t("details.noDetailedData")}
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-sm text-muted-foreground">{t("details.storageTip")}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 右侧:信息卡片(占 1 列)*/}
|
|
|
|
|
- {sessionStats && (
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- {/* Session 概览卡片 */}
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle className="text-base">{t("details.overview")}</CardTitle>
|
|
|
|
|
- <CardDescription>{t("details.overviewDescription")}</CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent className="space-y-3">
|
|
|
|
|
- {/* 请求数量 */}
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.totalRequests")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <Badge variant="secondary" className="font-mono font-semibold">
|
|
|
|
|
- <Hash className="h-3 w-3 mr-1" />
|
|
|
|
|
- {sessionStats.requestCount}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 时间跨度 */}
|
|
|
|
|
- {sessionStats.firstRequestAt && sessionStats.lastRequestAt && (
|
|
|
|
|
- <>
|
|
|
|
|
- <div className="border-t my-3" />
|
|
|
|
|
- <div className="flex flex-col gap-2">
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.firstRequest")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-xs font-mono">
|
|
|
|
|
- {new Date(sessionStats.firstRequestAt).toLocaleString("zh-CN")}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.lastRequest")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-xs font-mono">
|
|
|
|
|
- {new Date(sessionStats.lastRequestAt).toLocaleString("zh-CN")}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 总耗时 */}
|
|
|
|
|
- {sessionStats.totalDurationMs > 0 && (
|
|
|
|
|
- <>
|
|
|
|
|
- <div className="border-t my-3" />
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.totalDuration")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-sm font-mono font-semibold">
|
|
|
|
|
- {sessionStats.totalDurationMs < 1000
|
|
|
|
|
- ? `${sessionStats.totalDurationMs}ms`
|
|
|
|
|
- : `${(Number(sessionStats.totalDurationMs) / 1000).toFixed(2)}s`}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
-
|
|
|
|
|
- {/* 供应商和模型卡片 */}
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle className="text-base">{t("details.providersAndModels")}</CardTitle>
|
|
|
|
|
- <CardDescription>
|
|
|
|
|
- {t("details.providersAndModelsDescription")}
|
|
|
|
|
- </CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent className="space-y-3">
|
|
|
|
|
- {/* 供应商列表 */}
|
|
|
|
|
- {sessionStats.providers.length > 0 && (
|
|
|
|
|
- <div className="flex flex-col gap-2">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.providers")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
|
|
- {sessionStats.providers.map(
|
|
|
|
|
- (provider: { id: number; name: string }) => (
|
|
|
|
|
- <Badge key={provider.id} variant="outline" className="text-xs">
|
|
|
|
|
- {provider.name}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- )
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 模型列表 */}
|
|
|
|
|
- {sessionStats.models.length > 0 && (
|
|
|
|
|
- <>
|
|
|
|
|
- <div className="border-t my-3" />
|
|
|
|
|
- <div className="flex flex-col gap-2">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.models")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
|
|
- {sessionStats.models.map((model: string, idx: number) => (
|
|
|
|
|
- <Badge key={idx} variant="secondary" className="text-xs font-mono">
|
|
|
|
|
- {model}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
-
|
|
|
|
|
- {/* Token 使用卡片 */}
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle className="text-base">{t("details.tokenUsage")}</CardTitle>
|
|
|
|
|
- <CardDescription>{t("details.tokenUsageDescription")}</CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent className="space-y-3">
|
|
|
|
|
- {sessionStats.totalInputTokens > 0 && (
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.totalInput")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-sm font-mono">
|
|
|
|
|
- {sessionStats.totalInputTokens.toLocaleString()}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {sessionStats.totalOutputTokens > 0 && (
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.totalOutput")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-sm font-mono">
|
|
|
|
|
- {sessionStats.totalOutputTokens.toLocaleString()}
|
|
|
|
|
- </code>
|
|
|
|
|
|
|
+ {/* Empty State */}
|
|
|
|
|
+ {!sessionStats?.userAgent &&
|
|
|
|
|
+ !messages &&
|
|
|
|
|
+ !requestBody &&
|
|
|
|
|
+ !response &&
|
|
|
|
|
+ !requestHeaders && (
|
|
|
|
|
+ <div className="text-center py-20 border-2 border-dashed rounded-xl bg-muted/10">
|
|
|
|
|
+ <div className="text-muted-foreground text-lg mb-2 font-medium">
|
|
|
|
|
+ {t("details.noDetailedData")}
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {sessionStats.totalCacheCreationTokens > 0 && (
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground flex items-center gap-2">
|
|
|
|
|
- {t("details.cacheCreation")}
|
|
|
|
|
- {sessionStats.cacheTtlApplied && (
|
|
|
|
|
- <Badge variant="outline" className="text-xs">
|
|
|
|
|
- {sessionStats.cacheTtlApplied === "mixed"
|
|
|
|
|
- ? t("details.cacheTtlMixed")
|
|
|
|
|
- : sessionStats.cacheTtlApplied}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- )}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-sm font-mono">
|
|
|
|
|
- {sessionStats.totalCacheCreationTokens.toLocaleString()}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {sessionStats.totalCacheReadTokens > 0 && (
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.cacheRead")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-sm font-mono">
|
|
|
|
|
- {sessionStats.totalCacheReadTokens.toLocaleString()}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {totalTokens > 0 && (
|
|
|
|
|
- <>
|
|
|
|
|
- <div className="border-t my-3" />
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm font-semibold">{t("details.total")}</span>
|
|
|
|
|
- <code className="text-sm font-mono font-semibold">
|
|
|
|
|
- {totalTokens.toLocaleString()}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
-
|
|
|
|
|
- {/* 成本信息卡片 */}
|
|
|
|
|
- {sessionStats.totalCostUsd && parseFloat(sessionStats.totalCostUsd) > 0 && (
|
|
|
|
|
- <Card>
|
|
|
|
|
- <CardHeader>
|
|
|
|
|
- <CardTitle className="text-base">{t("details.costInfo")}</CardTitle>
|
|
|
|
|
- <CardDescription>{t("details.costInfoDescription")}</CardDescription>
|
|
|
|
|
- </CardHeader>
|
|
|
|
|
- <CardContent className="space-y-3">
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {t("details.totalFee")}
|
|
|
|
|
- </span>
|
|
|
|
|
- <code className="text-lg font-mono font-semibold text-green-600">
|
|
|
|
|
- {formatCurrency(sessionStats.totalCostUsd, currencyCode, 6)}
|
|
|
|
|
- </code>
|
|
|
|
|
- </div>
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">{t("details.storageTip")}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Right Sidebar: Stats (Desktop Only) */}
|
|
|
|
|
+ {sessionStats && (
|
|
|
|
|
+ <aside className="w-80 border-l bg-muted/5 overflow-y-auto hidden xl:block p-6">
|
|
|
|
|
+ <h3 className="font-semibold mb-4 text-sm uppercase tracking-wider text-muted-foreground">
|
|
|
|
|
+ {t("details.overview")}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <SessionStats stats={sessionStats} currencyCode={currencyCode} />
|
|
|
|
|
+ </aside>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </main>
|
|
|
|
|
|
|
|
- {/* 终止 Session 确认对话框 */}
|
|
|
|
|
|
|
+ {/* Terminate Dialog */}
|
|
|
<AlertDialog open={showTerminateDialog} onOpenChange={setShowTerminateDialog}>
|
|
<AlertDialog open={showTerminateDialog} onOpenChange={setShowTerminateDialog}>
|
|
|
<AlertDialogContent>
|
|
<AlertDialogContent>
|
|
|
<AlertDialogHeader>
|
|
<AlertDialogHeader>
|
|
|
<AlertDialogTitle>{t("actions.terminateSessionTitle")}</AlertDialogTitle>
|
|
<AlertDialogTitle>{t("actions.terminateSessionTitle")}</AlertDialogTitle>
|
|
|
<AlertDialogDescription>
|
|
<AlertDialogDescription>
|
|
|
{t("actions.terminateSessionDescription")}
|
|
{t("actions.terminateSessionDescription")}
|
|
|
- <br />
|
|
|
|
|
- <code className="text-xs font-mono mt-2 block">{sessionId}</code>
|
|
|
|
|
|
|
+ <div className="mt-2 p-2 bg-muted rounded font-mono text-xs break-all">
|
|
|
|
|
+ {sessionId}
|
|
|
|
|
+ </div>
|
|
|
</AlertDialogDescription>
|
|
</AlertDialogDescription>
|
|
|
</AlertDialogHeader>
|
|
</AlertDialogHeader>
|
|
|
<AlertDialogFooter>
|
|
<AlertDialogFooter>
|
|
@@ -656,6 +572,7 @@ export function SessionMessagesClient() {
|
|
|
onClick={handleTerminateSession}
|
|
onClick={handleTerminateSession}
|
|
|
disabled={isTerminating}
|
|
disabled={isTerminating}
|
|
|
>
|
|
>
|
|
|
|
|
+ {isTerminating ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
|
|
|
{isTerminating ? t("actions.terminating") : t("actions.confirmTerminate")}
|
|
{isTerminating ? t("actions.terminating") : t("actions.confirmTerminate")}
|
|
|
</AlertDialogAction>
|
|
</AlertDialogAction>
|
|
|
</AlertDialogFooter>
|
|
</AlertDialogFooter>
|
|
@@ -664,3 +581,23 @@ export function SessionMessagesClient() {
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// Helper icons
|
|
|
|
|
+function Loader2(props: React.ComponentProps<"svg">) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <svg
|
|
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
+ width="24"
|
|
|
|
|
+ height="24"
|
|
|
|
|
+ viewBox="0 0 24 24"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ stroke="currentColor"
|
|
|
|
|
+ strokeWidth="2"
|
|
|
|
|
+ strokeLinecap="round"
|
|
|
|
|
+ strokeLinejoin="round"
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ >
|
|
|
|
|
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|