Преглед изворни кода

feat(ui): redesign session details page with improved layout and UX

- Redesign request list sidebar with minimized/expanded views
- Add icons to tabs and make tab list horizontally scrollable
- Add copy response button to session details
- Enhance code-display component with copy functionality
- Update storageTip i18n messages (zh-CN, en, ja, ru, zh-TW)
- Add new dashboard-main wrapper component
- Add session-stats component for request statistics
ding113 пре 1 месец
родитељ
комит
f52a265121

+ 3 - 1
messages/en/dashboard.json

@@ -380,7 +380,7 @@
       "providers": "Providers",
       "models": "Models",
       "noDetailedData": "No detailed data available",
-      "storageTip": "Tip: Set environment variable STORE_SESSION_MESSAGES=true to enable messages and response storage",
+      "storageTip": "No detailed data found. To view request details, please check if the environment variable STORE_SESSION_MESSAGES is set to true. Note: Enabling this increases Redis memory usage and may include sensitive information.",
       "clientInfo": "Client Info",
       "requestHeaders": "Request Headers",
       "requestBody": "Request Body",
@@ -398,6 +398,8 @@
     "actions": {
       "back": "Back",
       "view": "View",
+      "copy": "Copy",
+      "download": "Download",
       "copyMessages": "Copy Request (Headers + Body)",
       "downloadMessages": "Download Request (Headers + Body)",
       "copied": "Copied",

+ 3 - 1
messages/ja/dashboard.json

@@ -379,7 +379,7 @@
       "providers": "プロバイダー",
       "models": "モデル",
       "noDetailedData": "詳細データなし",
-      "storageTip": "ヒント: メッセージとレスポンスの保存を有効にするには、環境変数 STORE_SESSION_MESSAGES=true を設定してください",
+      "storageTip": "詳細データが見つかりません。リクエストの詳細を表示するには、環境変数 STORE_SESSION_MESSAGES が true に設定されているか確認してください。注意:有効にすると Redis のメモリ使用量が増加し、機密情報が含まれる可能性があります。",
       "clientInfo": "クライアント情報",
       "requestHeaders": "リクエストヘッダー",
       "requestBody": "リクエストボディ",
@@ -397,6 +397,8 @@
     "actions": {
       "back": "戻る",
       "view": "表示",
+      "copy": "コピー",
+      "download": "ダウンロード",
       "copyMessages": "リクエスト(ヘッダーとボディ)をコピー",
       "downloadMessages": "リクエスト(ヘッダーとボディ)をダウンロード",
       "copied": "コピーしました",

+ 3 - 1
messages/ru/dashboard.json

@@ -379,7 +379,7 @@
       "providers": "Поставщики",
       "models": "Модели",
       "noDetailedData": "Подробные данные отсутствуют",
-      "storageTip": "Подсказка: установите переменную окружения STORE_SESSION_MESSAGES=true, чтобы включить сохранение сообщений и ответов",
+      "storageTip": "Подробные данные не найдены. Чтобы просмотреть детали запроса, проверьте, установлена ли переменная окружения STORE_SESSION_MESSAGES в значение true. Примечание: включение увеличит использование памяти Redis и может содержать конфиденциальную информацию.",
       "clientInfo": "Информация о клиенте",
       "requestHeaders": "Заголовки запроса",
       "requestBody": "Тело запроса",
@@ -397,6 +397,8 @@
     "actions": {
       "back": "Назад",
       "view": "Просмотр",
+      "copy": "Копировать",
+      "download": "Скачать",
       "copyMessages": "Копировать запрос (заголовки и тело)",
       "downloadMessages": "Скачать запрос (заголовки и тело)",
       "copied": "Скопировано",

+ 3 - 1
messages/zh-CN/dashboard.json

@@ -380,7 +380,7 @@
       "providers": "供应商",
       "models": "模型",
       "noDetailedData": "暂无详细数据",
-      "storageTip": "提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 和 response 存储",
+      "storageTip": "未找到详细数据。如需查看请求详情,请检查环境变量 STORE_SESSION_MESSAGES 是否已设置为 true。注意:启用后会增加 Redis 内存使用,且可能包含敏感信息。",
       "clientInfo": "客户端信息",
       "requestHeaders": "请求头",
       "requestBody": "请求体",
@@ -398,6 +398,8 @@
     "actions": {
       "back": "返回",
       "view": "查看",
+      "copy": "复制",
+      "download": "下载",
       "copyMessages": "复制请求头和请求体",
       "downloadMessages": "下载请求头和请求体",
       "copied": "已复制",

+ 3 - 1
messages/zh-TW/dashboard.json

@@ -380,7 +380,7 @@
       "providers": "供應商",
       "models": "模型",
       "noDetailedData": "暫無詳細資料",
-      "storageTip": "提示:請設定環境變數 STORE_SESSION_MESSAGES=true 以啟用 messages 和 response 儲存",
+      "storageTip": "未找到詳細資料。如需查看請求詳情,請檢查環境變數 STORE_SESSION_MESSAGES 是否已設定為 true。注意:啟用後會增加 Redis 記憶體使用,且可能包含敏感資訊。",
       "clientInfo": "用戶端資訊",
       "requestHeaders": "請求頭",
       "requestBody": "請求體",
@@ -398,6 +398,8 @@
     "actions": {
       "back": "返回",
       "view": "檢視",
+      "copy": "複製",
+      "download": "下載",
       "copyMessages": "複製請求頭與請求體",
       "downloadMessages": "下載請求頭與請求體",
       "copied": "已複製",

+ 25 - 0
src/app/[locale]/dashboard/_components/dashboard-main.tsx

@@ -0,0 +1,25 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { usePathname } from "@/i18n/routing";
+
+interface DashboardMainProps {
+  children: ReactNode;
+}
+
+export function DashboardMain({ children }: DashboardMainProps) {
+  const pathname = usePathname();
+
+  // Pattern to match /dashboard/sessions/[id]/messages
+  // The usePathname hook from next-intl/routing might return the path without locale prefix if configured that way,
+  // or we just check for the suffix.
+  // Let's be safe and check if it includes "/dashboard/sessions/" and ends with "/messages"
+  const isSessionMessagesPage =
+    pathname.includes("/dashboard/sessions/") && pathname.endsWith("/messages");
+
+  if (isSessionMessagesPage) {
+    return <main className="h-[calc(100vh-64px)] w-full overflow-hidden">{children}</main>;
+  }
+
+  return <main className="mx-auto w-full max-w-7xl px-6 py-8">{children}</main>;
+}

+ 2 - 1
src/app/[locale]/dashboard/layout.tsx

@@ -3,6 +3,7 @@ import { redirect } from "@/i18n/routing";
 
 import { getSession } from "@/lib/auth";
 import { DashboardHeader } from "./_components/dashboard-header";
+import { DashboardMain } from "./_components/dashboard-main";
 import { WebhookMigrationDialog } from "./_components/webhook-migration-dialog";
 
 export default async function DashboardLayout({
@@ -28,7 +29,7 @@ export default async function DashboardLayout({
   return (
     <div className="min-h-screen bg-background">
       <DashboardHeader session={session} />
-      <main className="mx-auto w-full max-w-7xl px-6 py-8">{children}</main>
+      <DashboardMain>{children}</DashboardMain>
       <WebhookMigrationDialog />
     </div>
   );

+ 162 - 151
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx

@@ -4,16 +4,16 @@ import {
   AlertCircle,
   ArrowDownUp,
   CheckCircle,
-  ChevronLeft,
-  ChevronRight,
-  Clock,
   Loader2,
+  MoreHorizontal,
+  Search,
 } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { getSessionRequests } from "@/actions/active-sessions";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
 import { Skeleton } from "@/components/ui/skeleton";
 import { cn } from "@/lib/utils";
 
@@ -34,19 +34,15 @@ interface RequestListSidebarProps {
   selectedSeq: number | null;
   onSelect: (seq: number) => void;
   collapsed?: boolean;
-  onCollapsedChange?: (collapsed: boolean) => void;
+  className?: string;
 }
 
-/**
- * Request List Sidebar - Session 内请求列表侧边栏
- * 显示 Session 中所有请求,支持分页和选择
- */
 export function RequestListSidebar({
   sessionId,
   selectedSeq,
   onSelect,
   collapsed = false,
-  onCollapsedChange,
+  className,
 }: RequestListSidebarProps) {
   const t = useTranslations("dashboard.sessions");
   const [requests, setRequests] = useState<RequestItem[]>([]);
@@ -86,210 +82,225 @@ export function RequestListSidebar({
     void fetchRequests(page, order);
   }, [fetchRequests, page, order]);
 
-  // 格式化相对时间
-  const formatRelativeTime = (date: Date | null) => {
+  // Formatter functions
+  const formatTime = (date: Date | null) => {
     if (!date) return "-";
-    const now = new Date();
-    const diff = now.getTime() - new Date(date).getTime();
-    const minutes = Math.floor(diff / 60000);
-    const hours = Math.floor(minutes / 60);
-    const days = Math.floor(hours / 24);
-
-    if (days > 0) return `${days}d`;
-    if (hours > 0) return `${hours}h`;
-    if (minutes > 0) return `${minutes}m`;
-    return "<1m";
+    return new Date(date).toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+      hour12: false,
+    });
   };
 
-  const formatRequestTimestamp = (date: Date | null) => {
-    if (!date) return "-";
-    const d = new Date(date);
-    if (Number.isNaN(d.getTime())) return "-";
-
-    const now = new Date();
-    const sameDay =
-      d.getFullYear() === now.getFullYear() &&
-      d.getMonth() === now.getMonth() &&
-      d.getDate() === now.getDate();
 
-    const pad2 = (v: number) => String(v).padStart(2, "0");
-    const pad3 = (v: number) => String(v).padStart(3, "0");
-    const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
 
-    if (sameDay) return time;
-    return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${time}`;
+  const getStatusColor = (statusCode: number | null) => {
+    if (!statusCode) return "text-muted-foreground";
+    if (statusCode >= 200 && statusCode < 300) return "text-emerald-600 dark:text-emerald-500";
+    if (statusCode >= 400 && statusCode < 500) return "text-amber-600 dark:text-amber-500";
+    return "text-destructive";
   };
 
-  // 获取状态图标
   const getStatusIcon = (statusCode: number | null) => {
-    if (!statusCode) {
-      return <Loader2 className="h-3 w-3 text-muted-foreground animate-spin" />;
-    }
-    if (statusCode >= 200 && statusCode < 300) {
-      return <CheckCircle className="h-3 w-3 text-green-600" />;
-    }
-    return <AlertCircle className="h-3 w-3 text-destructive" />;
+    if (!statusCode) return <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />;
+    if (statusCode >= 200 && statusCode < 300)
+      return <CheckCircle className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-500" />;
+    return <AlertCircle className="h-3.5 w-3.5 text-destructive" />;
   };
 
-  // 折叠时只显示切换按钮
+  // Minimized View
   if (collapsed) {
     return (
-      <div className="w-10 border-r bg-muted/30 flex flex-col items-center py-2">
-        <Button
-          variant="ghost"
-          size="icon"
-          className="h-8 w-8"
-          onClick={() => onCollapsedChange?.(false)}
-          aria-label={t("requestList.title")}
-        >
-          <ChevronRight className="h-4 w-4" />
-        </Button>
-        {total > 0 && (
-          <Badge variant="secondary" className="mt-2 text-xs px-1.5">
-            {total}
-          </Badge>
-        )}
+      <div className={cn("flex flex-col items-center py-4 border-r bg-muted/10 h-full", className)}>
+        <div className="flex flex-col gap-4 w-full px-2">
+          {isLoading
+            ? Array.from({ length: 5 }).map((_, i) => (
+                <Skeleton key={i} className="h-8 w-8 rounded-full" />
+              ))
+            : requests.map((req) => (
+                <button
+                  key={req.id}
+                  type="button"
+                  onClick={() => onSelect(req.sequence)}
+                  className={cn(
+                    "relative flex items-center justify-center w-8 h-8 rounded-full transition-all",
+                    selectedSeq === req.sequence
+                      ? "bg-primary text-primary-foreground shadow-sm"
+                      : "hover:bg-muted"
+                  )}
+                  title={`#${req.sequence} - ${req.model || "Unknown"}`}
+                >
+                  <span className="text-xs font-mono">{req.sequence}</span>
+                  <span className="absolute -top-1 -right-1">
+                    {/* Tiny status dot */}
+                    <span
+                      className={cn(
+                        "flex h-2 w-2 rounded-full",
+                        req.statusCode && req.statusCode >= 200 && req.statusCode < 300
+                          ? "bg-emerald-500"
+                          : !req.statusCode
+                            ? "bg-gray-400"
+                            : "bg-destructive"
+                      )}
+                    />
+                  </span>
+                </button>
+              ))}
+        </div>
       </div>
     );
   }
 
+  // Expanded View
   return (
-    <div className="w-64 border-r bg-muted/30 flex flex-col">
+    <div
+      className={cn("flex flex-col h-full bg-background/50 backdrop-blur-sm border-r", className)}
+    >
       {/* Header */}
-      <div className="p-3 border-b flex items-center justify-between">
-        <div className="flex items-center gap-2">
-          <h3 className="text-sm font-semibold">{t("requestList.title")}</h3>
-          {total > 0 && (
-            <Badge variant="secondary" className="text-xs">
-              {total}
-            </Badge>
-          )}
+      <div className="p-4 border-b flex flex-col gap-3">
+        <div className="flex items-center justify-between">
+          <h3 className="font-semibold tracking-tight">{t("requestList.title")}</h3>
+          <Badge variant="outline" className="text-xs font-mono">
+            {total}
+          </Badge>
         </div>
-        <div className="flex items-center gap-1">
-          {/* 排序切换按钮 */}
+        <div className="flex items-center gap-2">
+          {/* Placeholder for future search - currently just visual */}
+          <div className="relative flex-1">
+            <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
+            <Input
+              placeholder="Search..."
+              className="h-7 text-xs pl-7 bg-muted/20 border-muted-foreground/20"
+              disabled
+            />
+          </div>
           <Button
             variant="ghost"
             size="icon"
-            className="h-6 w-6"
+            className="h-7 w-7"
             onClick={() => {
               setOrder((prev) => (prev === "asc" ? "desc" : "asc"));
-              setPage(1); // 切换排序时重置到第一页
+              setPage(1);
             }}
             title={order === "asc" ? t("requestList.orderDesc") : t("requestList.orderAsc")}
           >
-            <ArrowDownUp className="h-4 w-4" />
-          </Button>
-          {/* 折叠按钮 */}
-          <Button
-            variant="ghost"
-            size="icon"
-            className="h-6 w-6"
-            onClick={() => onCollapsedChange?.(true)}
-          >
-            <ChevronLeft className="h-4 w-4" />
+            <ArrowDownUp className="h-3.5 w-3.5" />
           </Button>
         </div>
       </div>
 
-      {/* Request List */}
-      <div className="flex-1 overflow-y-auto">
-        <div className="p-2 space-y-1">
-          {isLoading && requests.length === 0 ? (
-            // Loading skeleton
-            Array.from({ length: 5 }).map((_, i) => (
-              <div key={i} className="p-2 rounded-md">
-                <Skeleton className="h-4 w-16 mb-1" />
-                <Skeleton className="h-3 w-24" />
+      {/* List */}
+      <div className="flex-1 overflow-y-auto scrollbar-thin">
+        {isLoading && requests.length === 0 ? (
+          <div className="p-4 space-y-3">
+            {Array.from({ length: 6 }).map((_, i) => (
+              <div key={i} className="flex gap-3">
+                <Skeleton className="h-8 w-8 rounded-full" />
+                <div className="space-y-1 flex-1">
+                  <Skeleton className="h-3 w-3/4" />
+                  <Skeleton className="h-2 w-1/2" />
+                </div>
               </div>
-            ))
-          ) : error ? (
-            <div className="p-4 text-center text-sm text-destructive">{error}</div>
-          ) : requests.length === 0 ? (
-            <div className="p-4 text-center text-sm text-muted-foreground">
-              {t("requestList.noRequests")}
-            </div>
-          ) : (
-            requests.map((request) => (
+            ))}
+          </div>
+        ) : error ? (
+          <div className="flex flex-col items-center justify-center h-40 text-center p-4">
+            <AlertCircle className="h-8 w-8 text-destructive mb-2 opacity-50" />
+            <p className="text-sm text-muted-foreground">{error}</p>
+          </div>
+        ) : requests.length === 0 ? (
+          <div className="flex flex-col items-center justify-center h-40 text-center p-4">
+            <MoreHorizontal className="h-8 w-8 text-muted-foreground mb-2 opacity-50" />
+            <p className="text-sm text-muted-foreground">{t("requestList.noRequests")}</p>
+          </div>
+        ) : (
+          <div className="divide-y divide-border/40">
+            {requests.map((request) => (
               <button
                 key={request.id}
                 type="button"
                 className={cn(
-                  "w-full p-2 rounded-md text-left transition-colors",
-                  "hover:bg-accent hover:text-accent-foreground",
-                  selectedSeq === request.sequence && "bg-accent text-accent-foreground"
+                  "w-full px-4 py-3 text-left transition-all hover:bg-muted/50 group relative",
+                  selectedSeq === request.sequence && "bg-muted/60 hover:bg-muted/70"
                 )}
                 onClick={() => onSelect(request.sequence)}
               >
-                <div className="flex items-center justify-between">
-                  <div className="flex items-center gap-1.5">
-                    {getStatusIcon(request.statusCode)}
+                {/* Active Indicator */}
+                {selectedSeq === request.sequence && (
+                  <div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />
+                )}
+
+                <div className="flex justify-between items-start mb-1">
+                  <div className="flex items-center gap-2">
                     <span
-                      className="text-sm font-medium font-mono tabular-nums"
-                      title={
-                        request.createdAt ? new Date(request.createdAt).toISOString() : undefined
-                      }
+                      className={cn(
+                        "text-xs font-mono font-medium px-1.5 py-0.5 rounded-md bg-muted",
+                        selectedSeq === request.sequence ? "bg-background shadow-sm" : ""
+                      )}
                     >
-                      {formatRequestTimestamp(request.createdAt)}
-                    </span>
-                  </div>
-                  <div className="flex items-center gap-1 text-xs text-muted-foreground">
-                    <Clock className="h-3 w-3" />
-                    {formatRelativeTime(request.createdAt)}
-                  </div>
-                </div>
-                <div className="mt-1 flex items-center justify-between">
-                  <span className="text-xs text-muted-foreground font-mono truncate max-w-[120px]">
-                    {request.model || "-"}{" "}
-                    <span className="text-[10px] text-muted-foreground/70">
                       #{request.sequence}
                     </span>
-                  </span>
-                  {request.statusCode && (
-                    <Badge
-                      variant="outline"
+                    <span
                       className={cn(
-                        "text-[10px] px-1 py-0",
-                        request.statusCode >= 200 && request.statusCode < 300
-                          ? "border-green-300 text-green-700 dark:border-green-700 dark:text-green-400"
-                          : "border-red-300 text-red-700 dark:border-red-700 dark:text-red-400"
+                        "text-xs font-medium truncate max-w-[120px]",
+                        !request.model && "text-muted-foreground italic"
                       )}
                     >
-                      {request.statusCode}
-                    </Badge>
-                  )}
+                      {request.model || "Unknown Model"}
+                    </span>
+                  </div>
+                  <span className="text-[10px] text-muted-foreground font-mono">
+                    {formatTime(request.createdAt)}
+                  </span>
+                </div>
+
+                <div className="flex justify-between items-center mt-2">
+                  <div className="flex items-center gap-1.5">
+                    {getStatusIcon(request.statusCode)}
+                    <span className={cn("text-xs font-mono", getStatusColor(request.statusCode))}>
+                      {request.statusCode || "---"}
+                    </span>
+                  </div>
+
+                  <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
+                    {request.costUsd && <span>${Number(request.costUsd).toFixed(6)}</span>}
+                  </div>
                 </div>
               </button>
-            ))
-          )}
-        </div>
+            ))}
+          </div>
+        )}
       </div>
 
-      {/* Pagination */}
-      {total > pageSize && (
-        <div className="p-2 border-t flex items-center justify-between">
+      {/* Pagination Footer */}
+      <div className="p-3 border-t bg-muted/10 backdrop-blur-sm">
+        <div className="flex items-center justify-between">
           <Button
-            variant="ghost"
-            size="sm"
-            className="h-7 text-xs"
+            variant="outline"
+            size="icon"
+            className="h-7 w-7"
             disabled={page === 1 || isLoading}
             onClick={() => setPage((p) => p - 1)}
           >
-            {t("requestList.prev")}
+            <span className="sr-only">{t("requestList.prev")}</span>
+            <span className="text-xs">←</span>
           </Button>
-          <span className="text-xs text-muted-foreground">
-            {page}/{Math.ceil(total / pageSize)}
+          <span className="text-xs text-muted-foreground font-mono">
+            {page} / {Math.max(1, Math.ceil(total / pageSize))}
           </span>
           <Button
-            variant="ghost"
-            size="sm"
-            className="h-7 text-xs"
+            variant="outline"
+            size="icon"
+            className="h-7 w-7"
             disabled={!hasMore || isLoading}
             onClick={() => setPage((p) => p + 1)}
           >
-            {t("requestList.next")}
+            <span className="sr-only">{t("requestList.next")}</span>
+            <span className="text-xs">→</span>
           </Button>
         </div>
-      )}
+      </div>
     </div>
   );
 }

+ 249 - 122
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-details-tabs.tsx

@@ -1,9 +1,22 @@
 "use client";
 
+import {
+  ArrowDownLeft,
+  ArrowUpRight,
+  Braces,
+  Check,
+  Copy,
+  Inbox,
+  MessageSquare,
+  Settings2,
+  Terminal,
+} from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useMemo } from "react";
+import { Button } from "@/components/ui/button";
 import { CodeDisplay } from "@/components/ui/code-display";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { isSSEText } from "@/lib/utils/sse";
 
 export type SessionMessages = Record<string, unknown> | Record<string, unknown>[];
@@ -38,6 +51,8 @@ interface SessionMessagesDetailsTabsProps {
   response: string | null;
   requestMeta: { clientUrl: string | null; upstreamUrl: string | null; method: string | null };
   responseMeta: { upstreamUrl: string | null; statusCode: number | null };
+  onCopyResponse?: () => void;
+  isResponseCopied?: boolean;
 }
 
 export function SessionMessagesDetailsTabs({
@@ -49,6 +64,8 @@ export function SessionMessagesDetailsTabs({
   responseHeaders,
   requestMeta,
   responseMeta,
+  onCopyResponse,
+  isResponseCopied,
 }: SessionMessagesDetailsTabsProps) {
   const t = useTranslations("dashboard.sessions");
   const codeExpandedMaxHeight = "calc(100vh - 260px)";
@@ -117,130 +134,240 @@ export function SessionMessagesDetailsTabs({
 
   const responseLanguage = response && isSSEText(response) ? "sse" : "json";
 
+  // Reusable Empty State Component
+  const EmptyState = ({ message }: { message: string }) => (
+    <div className="flex flex-col items-center justify-center py-16 text-muted-foreground bg-muted/20 rounded-lg border border-dashed text-center px-4">
+      <Inbox className="h-10 w-10 mb-3 opacity-20" />
+      <p className="text-sm max-w-lg">{message}</p>
+    </div>
+  );
+
   return (
-    <Tabs defaultValue="requestBody" className="w-full" data-testid="session-details-tabs">
-      <TabsList className="grid w-full grid-cols-6">
-        <TabsTrigger value="requestHeaders" data-testid="session-tab-trigger-request-headers">
-          {t("details.requestHeaders")}
-        </TabsTrigger>
-        <TabsTrigger value="requestBody" data-testid="session-tab-trigger-request-body">
-          {t("details.requestBody")}
-        </TabsTrigger>
-        <TabsTrigger value="requestMessages" data-testid="session-tab-trigger-request-messages">
-          {t("details.requestMessages")}
-        </TabsTrigger>
-        <TabsTrigger value="specialSettings" data-testid="session-tab-trigger-special-settings">
-          {t("details.specialSettings")}
-        </TabsTrigger>
-        <TabsTrigger value="responseHeaders" data-testid="session-tab-trigger-response-headers">
-          {t("details.responseHeaders")}
-        </TabsTrigger>
-        <TabsTrigger value="responseBody" data-testid="session-tab-trigger-response-body">
-          {t("details.responseBody")}
-        </TabsTrigger>
-      </TabsList>
-
-      <TabsContent value="requestHeaders" data-testid="session-tab-request-headers">
-        {formattedRequestHeaders === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noHeaders")}</div>
-        ) : (
-          <CodeDisplay
-            content={formattedRequestHeaders}
-            language="text"
-            fileName="request.headers"
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
-        )}
-      </TabsContent>
-
-      <TabsContent value="requestBody" data-testid="session-tab-request-body">
-        {requestBodyContent === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noData")}</div>
-        ) : (
-          <CodeDisplay
-            content={requestBodyContent}
-            language="json"
-            fileName="request.json"
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
-        )}
-      </TabsContent>
-
-      <TabsContent value="requestMessages" data-testid="session-tab-request-messages">
-        {requestMessagesContent === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noData")}</div>
-        ) : (
-          <CodeDisplay
-            content={requestMessagesContent}
-            language="json"
-            fileName="request.messages.json"
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
-        )}
-      </TabsContent>
-
-      <TabsContent value="specialSettings" data-testid="session-tab-special-settings">
-        {specialSettingsContent === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noData")}</div>
-        ) : (
-          <CodeDisplay
-            content={specialSettingsContent}
-            language="json"
-            fileName="specialSettings.json"
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
-        )}
-      </TabsContent>
-
-      <TabsContent value="responseHeaders" data-testid="session-tab-response-headers">
-        {formattedResponseHeaders === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noHeaders")}</div>
-        ) : (
-          <CodeDisplay
-            content={formattedResponseHeaders}
-            language="text"
-            fileName="response.headers"
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
-        )}
-      </TabsContent>
-
-      <TabsContent value="responseBody" data-testid="session-tab-response-body">
-        {response === null ? (
-          <div className="text-muted-foreground p-4">{t("details.noData")}</div>
-        ) : (
-          <CodeDisplay
-            content={response}
-            language={responseLanguage}
-            fileName={responseLanguage === "sse" ? "response.sse" : "response.json"}
-            maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
-            maxLines={SESSION_DETAILS_MAX_LINES}
-            maxHeight="600px"
-            defaultExpanded
-            expandedMaxHeight={codeExpandedMaxHeight}
-          />
+    <Tabs
+      defaultValue="requestBody"
+      className="w-full space-y-4"
+      data-testid="session-details-tabs"
+    >
+      {/* Scrollable Tabs List with Action Button */}
+      <div className="w-full flex items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
+        <TabsList className="w-max inline-flex h-auto p-1 items-center justify-start gap-1 bg-muted/50 rounded-lg">
+          <TabsTrigger
+            value="requestHeaders"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-request-headers"
+          >
+            <ArrowUpRight className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.requestHeaders")}</span>
+          </TabsTrigger>
+
+          <TabsTrigger
+            value="requestBody"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-request-body"
+          >
+            <Braces className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.requestBody")}</span>
+          </TabsTrigger>
+
+          <TabsTrigger
+            value="requestMessages"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-request-messages"
+          >
+            <MessageSquare className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.requestMessages")}</span>
+          </TabsTrigger>
+
+          <TabsTrigger
+            value="specialSettings"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-special-settings"
+          >
+            <Settings2 className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.specialSettings")}</span>
+          </TabsTrigger>
+
+          <div className="mx-1 w-px h-5 bg-border hidden sm:block" />
+
+          <TabsTrigger
+            value="responseHeaders"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-response-headers"
+          >
+            <ArrowDownLeft className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.responseHeaders")}</span>
+          </TabsTrigger>
+
+          <TabsTrigger
+            value="responseBody"
+            className="gap-2 px-3 py-2 data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-primary"
+            data-testid="session-tab-trigger-response-body"
+          >
+            <Terminal className="h-4 w-4" />
+            <span className="whitespace-nowrap">{t("details.responseBody")}</span>
+          </TabsTrigger>
+        </TabsList>
+
+        {/* Copy Response Button */}
+        {response && onCopyResponse && (
+          <TooltipProvider>
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Button
+                  variant="outline"
+                  size="sm"
+                  className="ml-auto h-9 px-3 gap-2 bg-background border-dashed text-muted-foreground hover:text-foreground shrink-0"
+                  onClick={onCopyResponse}
+                >
+                  {isResponseCopied ? (
+                    <Check className="h-4 w-4 text-green-500" />
+                  ) : (
+                    <Copy className="h-4 w-4" />
+                  )}
+                  <span className="text-xs font-medium">
+                    {isResponseCopied ? t("actions.copied") : t("actions.copyResponse")}
+                  </span>
+                </Button>
+              </TooltipTrigger>
+              <TooltipContent>{t("actions.copyResponse")}</TooltipContent>
+            </Tooltip>
+          </TooltipProvider>
         )}
-      </TabsContent>
+      </div>
+
+      <div className="border rounded-lg bg-card text-card-foreground shadow-sm overflow-hidden">
+        <TabsContent
+          value="requestHeaders"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-request-headers"
+        >
+          {formattedRequestHeaders === null ? (
+            <EmptyState message={t("details.storageTip")} />
+          ) : (
+            <CodeDisplay
+              content={formattedRequestHeaders}
+              language="text"
+              fileName="request.headers"
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+
+        <TabsContent
+          value="requestBody"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-request-body"
+        >
+          {requestBodyContent === null ? (
+            <EmptyState message={t("details.storageTip")} />
+          ) : (
+            <CodeDisplay
+              content={requestBodyContent}
+              language="json"
+              fileName="request.json"
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+
+        <TabsContent
+          value="requestMessages"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-request-messages"
+        >
+          {requestMessagesContent === null ? (
+            <EmptyState message={t("details.storageTip")} />
+          ) : (
+            <CodeDisplay
+              content={requestMessagesContent}
+              language="json"
+              fileName="request.messages.json"
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+
+        <TabsContent
+          value="specialSettings"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-special-settings"
+        >
+          {specialSettingsContent === null ? (
+            <EmptyState message={t("details.noData")} />
+          ) : (
+            <CodeDisplay
+              content={specialSettingsContent}
+              language="json"
+              fileName="specialSettings.json"
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+
+        <TabsContent
+          value="responseHeaders"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-response-headers"
+        >
+          {formattedResponseHeaders === null ? (
+            <EmptyState message={t("details.storageTip")} />
+          ) : (
+            <CodeDisplay
+              content={formattedResponseHeaders}
+              language="text"
+              fileName="response.headers"
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+
+        <TabsContent
+          value="responseBody"
+          className="m-0 focus-visible:outline-none"
+          data-testid="session-tab-response-body"
+        >
+          {response === null ? (
+            <EmptyState message={t("details.storageTip")} />
+          ) : (
+            <CodeDisplay
+              content={response}
+              language={responseLanguage}
+              fileName={responseLanguage === "sse" ? "response.sse" : "response.json"}
+              maxContentBytes={SESSION_DETAILS_MAX_CONTENT_BYTES}
+              maxLines={SESSION_DETAILS_MAX_LINES}
+              maxHeight="600px"
+              defaultExpanded
+              expandedMaxHeight={codeExpandedMaxHeight}
+              className="border-0 rounded-none"
+            />
+          )}
+        </TabsContent>
+      </div>
     </Tabs>
   );
 }

+ 297 - 360
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-messages-client.tsx

@@ -1,13 +1,22 @@
 "use client";
 
 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 { useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { toast } from "sonner";
 import { getSessionDetails, terminateActiveSession } from "@/actions/active-sessions";
-import { Section } from "@/components/section";
 import {
   AlertDialog,
   AlertDialogAction,
@@ -20,12 +29,20 @@ import {
 } from "@/components/ui/alert-dialog";
 import { Badge } from "@/components/ui/badge";
 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 { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
+import type { CurrencyCode } from "@/lib/utils/currency";
 import { RequestListSidebar } from "./request-list-sidebar";
 import { type SessionMessages, SessionMessagesDetailsTabs } from "./session-details-tabs";
 import { isSessionMessages } from "./session-messages-guards";
+import { SessionStats } from "./session-stats";
 
 async function fetchSystemSettings(): Promise<{
   currencyDisplay: CurrencyCode;
@@ -39,14 +56,14 @@ async function fetchSystemSettings(): Promise<{
 
 export function SessionMessagesClient() {
   const t = useTranslations("dashboard.sessions");
-  const tDesc = useTranslations("dashboard.description");
+
   const params = useParams();
   const searchParams = useSearchParams();
   const router = useRouter();
   const pathname = usePathname();
   const sessionId = params.sessionId as string;
 
-  // 从 URL 获取当前选中的请求序号
+  // URL state
   const seqParam = searchParams.get("seq");
   const selectedSeq = (() => {
     if (!seqParam) return null;
@@ -55,6 +72,7 @@ export function SessionMessagesClient() {
     return parsed;
   })();
 
+  // Data State
   const [messages, setMessages] = useState<SessionMessages | null>(null);
   const [requestBody, setRequestBody] = useState<unknown | null>(null);
   const [response, setResponse] = useState<string | null>(null);
@@ -83,6 +101,8 @@ export function SessionMessagesClient() {
   const [currentSequence, setCurrentSequence] = useState<number | null>(null);
   const [prevSequence, setPrevSequence] = useState<number | null>(null);
   const [nextSequence, setNextSequence] = useState<number | null>(null);
+
+  // UI State
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [copiedRequest, setCopiedRequest] = useState(false);
@@ -90,6 +110,8 @@ export function SessionMessagesClient() {
   const [showTerminateDialog, setShowTerminateDialog] = useState(false);
   const [isTerminating, setIsTerminating] = useState(false);
   const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+  const [isMobileStatsOpen, setIsMobileStatsOpen] = useState(false);
 
   const resetDetailsState = useCallback(() => {
     setMessages(null);
@@ -113,12 +135,12 @@ export function SessionMessagesClient() {
 
   const currencyCode = systemSettings?.currencyDisplay || "USD";
 
-  // 处理请求选择(更新 URL)
   const handleSelectRequest = useCallback(
     (seq: number) => {
       const params = new URLSearchParams(window.location.search);
       params.set("seq", seq.toString());
       router.replace(`${pathname}?${params.toString()}`);
+      setIsMobileMenuOpen(false);
     },
     [router, pathname]
   );
@@ -131,7 +153,6 @@ export function SessionMessagesClient() {
       setError(null);
 
       try {
-        // 传入 requestSequence 参数以获取特定请求的消息
         const result = await getSessionDetails(sessionId, selectedSeq ?? undefined);
         if (cancelled) return;
 
@@ -174,6 +195,7 @@ export function SessionMessagesClient() {
   const canExportRequest =
     !isLoading && error === null && requestHeaders !== null && requestBody !== null;
   const exportSequence = selectedSeq ?? currentSequence;
+
   const getRequestExportJson = () => {
     return JSON.stringify(
       {
@@ -191,31 +213,32 @@ export function SessionMessagesClient() {
 
   const handleCopyRequest = async () => {
     if (!canExportRequest) return;
-
     try {
       await navigator.clipboard.writeText(getRequestExportJson());
       setCopiedRequest(true);
       setTimeout(() => setCopiedRequest(false), 2000);
+      toast.success(t("actions.copied"));
     } catch (err) {
       console.error(t("errors.copyFailed"), err);
+      toast.error(t("errors.copyFailed"));
     }
   };
 
   const handleCopyResponse = async () => {
     if (!response) return;
-
     try {
       await navigator.clipboard.writeText(response);
       setCopiedResponse(true);
       setTimeout(() => setCopiedResponse(false), 2000);
+      toast.success(t("actions.copied"));
     } catch (err) {
       console.error(t("errors.copyFailed"), err);
+      toast.error(t("errors.copyFailed"));
     }
   };
 
   const handleDownloadRequest = () => {
     if (!canExportRequest) return;
-
     const jsonStr = getRequestExportJson();
     const blob = new Blob([jsonStr], { type: "application/json" });
     const url = URL.createObjectURL(blob);
@@ -235,7 +258,6 @@ export function SessionMessagesClient() {
       const result = await terminateActiveSession(sessionId);
       if (result.ok) {
         toast.success(t("actions.terminateSuccess"));
-        // 终止成功后返回列表页
         router.push("/dashboard/sessions");
       } else {
         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 (
-    <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")}
               </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 className="flex gap-2">
+          <div className="flex items-center gap-2">
+            {/* Desktop Actions */}
+            <div className="hidden sm:flex items-center gap-2">
               {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 && (
                 <Button
                   variant="destructive"
                   size="sm"
+                  className="h-8"
                   onClick={() => setShowTerminateDialog(true)}
-                  disabled={isTerminating}
                 >
                   <XCircle className="h-4 w-4 mr-2" />
                   {t("actions.terminate")}
                 </Button>
               )}
             </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
-                        variant="ghost"
+                        variant="outline"
                         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>
                     </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
                     messages={messages}
                     requestBody={requestBody}
@@ -389,264 +519,50 @@ export function SessionMessagesClient() {
                     responseHeaders={responseHeaders}
                     requestMeta={requestMeta}
                     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>
-                      )}
-
-                      {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>
+
+          {/* 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>
+      </main>
 
-      {/* 终止 Session 确认对话框 */}
+      {/* Terminate Dialog */}
       <AlertDialog open={showTerminateDialog} onOpenChange={setShowTerminateDialog}>
         <AlertDialogContent>
           <AlertDialogHeader>
             <AlertDialogTitle>{t("actions.terminateSessionTitle")}</AlertDialogTitle>
             <AlertDialogDescription>
               {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>
           </AlertDialogHeader>
           <AlertDialogFooter>
@@ -656,6 +572,7 @@ export function SessionMessagesClient() {
               onClick={handleTerminateSession}
               disabled={isTerminating}
             >
+              {isTerminating ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
               {isTerminating ? t("actions.terminating") : t("actions.confirmTerminate")}
             </AlertDialogAction>
           </AlertDialogFooter>
@@ -664,3 +581,23 @@ export function SessionMessagesClient() {
     </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>
+  );
+}

+ 199 - 0
src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-stats.tsx

@@ -0,0 +1,199 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import {
+  Calendar,
+  Clock,
+  Cpu,
+  Database,
+  DollarSign,
+  Hash,
+  Layers,
+  Server,
+  Zap,
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import { type CurrencyCode, formatCurrency } from "@/lib/utils/currency";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+
+interface SessionStatsProps {
+  stats: {
+    userAgent: string | null;
+    requestCount: number;
+    firstRequestAt: string | null;
+    lastRequestAt: string | null;
+    totalDurationMs: number;
+    providers: { id: number; name: string }[];
+    models: string[];
+    totalInputTokens: number;
+    totalOutputTokens: number;
+    totalCacheCreationTokens: number;
+    totalCacheReadTokens: number;
+    totalCostUsd: string | null;
+  };
+  currencyCode?: CurrencyCode;
+  className?: string;
+}
+
+export function SessionStats({ stats, currencyCode = "USD", className }: SessionStatsProps) {
+  const t = useTranslations("dashboard.sessions.details");
+
+  const totalTokens =
+    stats.totalInputTokens +
+    stats.totalOutputTokens +
+    stats.totalCacheCreationTokens +
+    stats.totalCacheReadTokens;
+
+  const durationSeconds = stats.totalDurationMs / 1000;
+  
+  const hasCost = stats.totalCostUsd && parseFloat(stats.totalCostUsd) > 0;
+
+  return (
+    <div className={cn("flex flex-col gap-6", className)}>
+      {/* Key Metrics Grid */}
+      <div className="grid grid-cols-2 gap-3">
+        {hasCost && (
+          <div className="col-span-2 bg-emerald-500/10 border border-emerald-500/20 rounded-lg p-3 flex flex-col gap-1">
+             <div className="flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400 font-medium">
+                <DollarSign className="h-3.5 w-3.5" />
+                <span>{t("totalFee")}</span>
+             </div>
+             <div className="text-xl font-bold font-mono text-emerald-700 dark:text-emerald-300">
+                {formatCurrency(stats.totalCostUsd, currencyCode, 6)}
+             </div>
+          </div>
+        )}
+        
+        <MetricCard 
+          icon={<Hash className="h-3.5 w-3.5" />}
+          label={t("totalRequests")}
+          value={stats.requestCount.toString()}
+        />
+        
+        <MetricCard 
+          icon={<Clock className="h-3.5 w-3.5" />}
+          label={t("totalDuration")}
+          value={durationSeconds < 1 ? "< 1s" : `${durationSeconds.toFixed(2)}s`}
+        />
+        
+        <MetricCard 
+          icon={<Zap className="h-3.5 w-3.5" />}
+          label={t("total")} // Total Tokens
+          value={totalTokens.toLocaleString()}
+          className="col-span-2"
+        />
+      </div>
+
+      <Separator />
+
+      {/* Token Breakdown - Compact List */}
+      <div className="space-y-3">
+        <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
+           <Layers className="h-3 w-3" />
+           {t("tokenUsage")}
+        </h4>
+        <div className="space-y-2 text-sm">
+           <TokenRow label={t("totalInput")} value={stats.totalInputTokens} />
+           <TokenRow label={t("totalOutput")} value={stats.totalOutputTokens} />
+           {(stats.totalCacheCreationTokens > 0 || stats.totalCacheReadTokens > 0) && (
+             <>
+               <div className="my-1 border-t border-dashed opacity-50" />
+               <TokenRow label="Cache Write" value={stats.totalCacheCreationTokens} icon={<Database className="h-3 w-3 text-muted-foreground"/>} />
+               <TokenRow label="Cache Read" value={stats.totalCacheReadTokens} icon={<Database className="h-3 w-3 text-muted-foreground"/>} />
+             </>
+           )}
+        </div>
+      </div>
+
+      <Separator />
+
+      {/* Tech Stack - Tags */}
+      <div className="space-y-3">
+         <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
+           <Cpu className="h-3 w-3" />
+           {t("providersAndModels")}
+         </h4>
+         
+         <div className="space-y-2">
+            {stats.providers.length > 0 && (
+              <div className="flex flex-wrap gap-1.5">
+                 {stats.providers.map(p => (
+                   <Badge key={p.id} variant="outline" className="text-[10px] px-1.5 py-0 h-5 font-normal bg-background">
+                     <Server className="h-2.5 w-2.5 mr-1 text-muted-foreground" />
+                     {p.name}
+                   </Badge>
+                 ))}
+              </div>
+            )}
+            
+            {stats.models.length > 0 && (
+               <div className="flex flex-wrap gap-1.5">
+                  {stats.models.map((m, i) => (
+                    <Badge key={i} variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-mono text-muted-foreground">
+                       {m}
+                    </Badge>
+                  ))}
+               </div>
+            )}
+         </div>
+      </div>
+
+      <Separator />
+
+      {/* Metadata / Time */}
+      <div className="space-y-3">
+         <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
+           <Calendar className="h-3 w-3" />
+           {t("overview")}
+         </h4>
+         
+         <div className="space-y-3">
+            <TimeRow label={t("firstRequest")} date={stats.firstRequestAt} />
+            <TimeRow label={t("lastRequest")} date={stats.lastRequestAt} />
+         </div>
+      </div>
+    </div>
+  );
+}
+
+function MetricCard({ icon, label, value, className }: { icon: React.ReactNode, label: string, value: string, className?: string }) {
+   return (
+      <div className={cn("bg-card border rounded-md p-2.5 flex flex-col gap-1 shadow-sm", className)}>
+         <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground uppercase font-medium">
+            {icon}
+            <span className="truncate">{label}</span>
+         </div>
+         <div className="text-lg font-semibold font-mono tracking-tight leading-none mt-0.5">
+            {value}
+         </div>
+      </div>
+   )
+}
+
+function TokenRow({ label, value, icon }: { label: string, value: number, icon?: React.ReactNode }) {
+   return (
+      <div className="flex items-center justify-between">
+         <div className="flex items-center gap-2 text-muted-foreground">
+            {icon}
+            <span>{label}</span>
+         </div>
+         <span className="font-mono font-medium">{value.toLocaleString()}</span>
+      </div>
+   )
+}
+
+function TimeRow({ label, date }: { label: string, date: string | null }) {
+   if (!date) return null;
+   const d = new Date(date);
+   return (
+      <div className="flex flex-col gap-0.5">
+         <span className="text-[10px] text-muted-foreground uppercase">{label}</span>
+         <div className="flex items-center justify-between text-xs font-mono">
+            <span>{d.toLocaleDateString()}</span>
+            <span className="text-muted-foreground">{d.toLocaleTimeString()}</span>
+         </div>
+      </div>
+   )
+}

+ 79 - 22
src/components/ui/code-display.tsx

@@ -1,6 +1,14 @@
 "use client";
 
-import { ChevronDown, ChevronUp, Download, File as FileIcon, Search } from "lucide-react";
+import {
+  Check,
+  ChevronDown,
+  ChevronUp,
+  Copy,
+  Download,
+  File as FileIcon,
+  Search,
+} from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useEffect, useMemo, useRef, useState } from "react";
 import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
@@ -9,6 +17,7 @@ import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { parseSSEDataForDisplay } from "@/lib/utils/sse";
 
 export type CodeDisplayLanguage = "json" | "sse" | "text";
@@ -26,6 +35,8 @@ export interface CodeDisplayProps {
   defaultExpanded?: boolean;
   maxContentBytes?: number;
   maxLines?: number;
+  enableDownload?: boolean;
+  enableCopy?: boolean;
 }
 
 function safeJsonParse(text: string): { ok: true; value: unknown } | { ok: false } {
@@ -70,8 +81,11 @@ export function CodeDisplay({
   defaultExpanded = false,
   maxContentBytes,
   maxLines,
+  enableDownload = true,
+  enableCopy = true,
 }: CodeDisplayProps) {
   const t = useTranslations("dashboard.sessions");
+  const tActions = useTranslations("dashboard.actions");
   const resolvedMaxContentBytes = maxContentBytes ?? DEFAULT_MAX_CONTENT_BYTES;
   const resolvedMaxLines = maxLines ?? DEFAULT_MAX_LINES;
   const contentBytes = useMemo(() => new Blob([content]).size, [content]);
@@ -92,6 +106,7 @@ export function CodeDisplay({
   const sseScrollRef = useRef<HTMLDivElement | null>(null);
   const [sseViewportHeight, setSseViewportHeight] = useState(0);
   const [sseScrollTop, setSseScrollTop] = useState(0);
+  const [copied, setCopied] = useState(false);
 
   useEffect(() => {
     const getTheme = () => (document.documentElement.classList.contains("dark") ? "dark" : "light");
@@ -187,31 +202,43 @@ export function CodeDisplay({
   const displayText = lineFilteredText ?? content;
   const contentMaxHeight = isExpanded ? expandedMaxHeight : maxHeight;
 
+  const downloadFileName =
+    fileName ??
+    (language === "json" ? "content.json" : language === "sse" ? "content.sse" : "content.txt");
+
+  const handleDownload = () => {
+    const blob = new Blob([content], {
+      type: language === "json" ? "application/json" : "text/plain",
+    });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    try {
+      a.href = url;
+      a.download = downloadFileName;
+      document.body.appendChild(a);
+      a.click();
+    } finally {
+      if (a.isConnected) {
+        document.body.removeChild(a);
+      }
+      URL.revokeObjectURL(url);
+    }
+  };
+
+  const handleCopy = async () => {
+    try {
+      await navigator.clipboard.writeText(content);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } catch (err) {
+      console.error("Copy failed", err);
+    }
+  };
+
   if (isHardLimited) {
     const sizeBytes = contentBytes;
     const sizeMB = (sizeBytes / 1_000_000).toFixed(2);
     const maxSizeMB = (resolvedMaxContentBytes / 1_000_000).toFixed(2);
-    const downloadFileName =
-      fileName ??
-      (language === "json" ? "content.json" : language === "sse" ? "content.sse" : "content.txt");
-    const handleDownload = () => {
-      const blob = new Blob([content], {
-        type: language === "json" ? "application/json" : "text/plain",
-      });
-      const url = URL.createObjectURL(blob);
-      const a = document.createElement("a");
-      try {
-        a.href = url;
-        a.download = downloadFileName;
-        document.body.appendChild(a);
-        a.click();
-      } finally {
-        if (a.isConnected) {
-          document.body.removeChild(a);
-        }
-        URL.revokeObjectURL(url);
-      }
-    };
 
     return (
       <div data-testid="code-display" className="rounded-md border bg-muted/30">
@@ -291,6 +318,36 @@ export function CodeDisplay({
         </Button>
       )}
 
+      {enableCopy && (
+        <TooltipProvider>
+          <Tooltip>
+            <TooltipTrigger asChild>
+              <Button variant="ghost" size="sm" onClick={handleCopy} className="h-9 w-9 p-0">
+                {copied ? (
+                  <Check className="h-4 w-4 text-green-500" />
+                ) : (
+                  <Copy className="h-4 w-4" />
+                )}
+              </Button>
+            </TooltipTrigger>
+            <TooltipContent>{copied ? tActions("copied") : tActions("copy")}</TooltipContent>
+          </Tooltip>
+        </TooltipProvider>
+      )}
+
+      {enableDownload && (
+        <TooltipProvider>
+          <Tooltip>
+            <TooltipTrigger asChild>
+              <Button variant="ghost" size="sm" onClick={handleDownload} className="h-9 w-9 p-0">
+                <Download className="h-4 w-4" />
+              </Button>
+            </TooltipTrigger>
+            <TooltipContent>{tActions("download")}</TooltipContent>
+          </Tooltip>
+        </TooltipProvider>
+      )}
+
       {isLargeContent && (
         <Button
           type="button"

+ 2 - 2
src/repository/message.ts

@@ -407,7 +407,7 @@ export async function aggregateSessionStats(sessionId: string): Promise<{
     lastRequestAt: stats.lastRequestAt,
     providers: providerList.map((p) => ({
       id: p.providerId!,
-      name: p.providerName || "未知",
+      name: p.providerName || `Provider #${p.providerId}`,
     })),
     models: modelList.map((m) => m.model!),
     userName: userInfo.userName,
@@ -505,7 +505,7 @@ export async function aggregateMultipleSessionStats(sessionIds: string[]): Promi
     }
     providersMap.get(p.sessionId)?.push({
       id: p.providerId!,
-      name: p.providerName || "未知",
+      name: p.providerName || `Provider #${p.providerId}`,
     });
   }