"use client"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { LogDataTable } from "@app/components/LogDataTable"; import { AiSessionChatView } from "@app/components/AiSessionChatView"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useTranslations } from "next-intl"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHref"; import { logQueries } from "@app/lib/queries"; import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat"; import { ColumnDef } from "@tanstack/react-table"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { ArrowUpRight, Bot, Waves, User } from "lucide-react"; import Link from "next/link"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import type { QueryAiSessionLogResponse } from "@server/routers/auditLogs/types"; const capabilityLabels: Record = { openai_chat: "OpenAI Chat Completions", openai_responses: "OpenAI Responses", anthropic_messages: "Anthropic Messages", gemini_generate_content: "Gemini", google_generate_content: "Vertex AI (Generate Content)", google_raw_predict: "Vertex AI (Raw Predict)", bedrock_model_invoke: "Bedrock (Invoke Model)", bedrock_converse: "Bedrock (Converse)" }; export default function AiSessionLogsPage() { const router = useRouter(); const api = createApiClient(useEnvContext()); const t = useTranslations(); const { orgId } = useParams(); const searchParams = useSearchParams(); const [isExporting, startTransition] = useTransition(); const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useStoredPageSize("ai-session-logs", 20); const [filters, setFilters] = useState<{ providerId?: string; capability?: string; resourceId?: string; actor?: string; virtualApiKeyId?: string; model?: string; isStream?: string; }>({ providerId: searchParams.get("providerId") || undefined, capability: searchParams.get("capability") || undefined, resourceId: searchParams.get("resourceId") || undefined, actor: searchParams.get("actor") || undefined, virtualApiKeyId: searchParams.get("virtualApiKeyId") || undefined, model: searchParams.get("model") || undefined, isStream: searchParams.get("isStream") || undefined }); const getDefaultDateRange = () => { const startParam = searchParams.get("start"); const endParam = searchParams.get("end"); if (startParam && endParam) { return { startDate: { date: new Date(startParam) }, endDate: { date: new Date(endParam) } }; } return { startDate: { date: getSevenDaysAgo() }, endDate: { date: new Date() } }; }; const [dateRange, setDateRange] = useState<{ startDate: DateTimeValue; endDate: DateTimeValue; }>(getDefaultDateRange()); const queryFilters = useMemo(() => { let timeStart: string | undefined; let timeEnd: string | undefined; if (dateRange.startDate?.date) { const dt = new Date(dateRange.startDate.date); if (dateRange.startDate.time) { const [h, m, s] = dateRange.startDate.time .split(":") .map(Number); dt.setHours(h, m, s || 0); } timeStart = dt.toISOString(); } if (dateRange.endDate?.date) { const dt = new Date(dateRange.endDate.date); if (dateRange.endDate.time) { const [h, m, s] = dateRange.endDate.time.split(":").map(Number); dt.setHours(h, m, s || 0); } else { const now = new Date(); dt.setHours( now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds() ); } timeEnd = dt.toISOString(); } return { timeStart, timeEnd, page: currentPage, pageSize, ...filters }; }, [dateRange, currentPage, pageSize, filters]); const { data, isFetching, isLoading, refetch } = useQuery({ ...logQueries.aiSessions({ orgId: orgId as string, filters: queryFilters }) }); const rows = isLoading ? generateSampleAiSessionLogs() : (data?.log ?? []); const totalCount = data?.pagination?.total ?? 0; const filterAttributes = data?.filterAttributes ?? { providers: [], resources: [], users: [], virtualApiKeys: [], models: [] }; const handleDateRangeChange = ( startDate: DateTimeValue, endDate: DateTimeValue ) => { setDateRange({ startDate, endDate }); setCurrentPage(0); updateUrlParamsForAllFilters({ start: startDate.date?.toISOString() || "", end: endDate.date?.toISOString() || "" }); }; const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); setCurrentPage(0); }; const handleFilterChange = ( filterType: keyof typeof filters, value: string | undefined ) => { const newFilters = { ...filters, [filterType]: value }; setFilters(newFilters); setCurrentPage(0); updateUrlParamsForAllFilters(newFilters); }; const updateUrlParamsForAllFilters = ( newFilters: | typeof filters | { start: string; end: string; } ) => { const params = new URLSearchParams(searchParams); Object.entries(newFilters).forEach(([key, value]) => { if (value) { params.set(key, value); } else { params.delete(key); } }); router.replace(`?${params.toString()}`, { scroll: false }); }; const exportData = async () => { try { const params: any = { timeStart: dateRange.startDate?.date ? new Date(dateRange.startDate.date).toISOString() : undefined, timeEnd: dateRange.endDate?.date ? new Date(dateRange.endDate.date).toISOString() : undefined, ...filters }; const response = await api.get(`/org/${orgId}/logs/ai/export`, { responseType: "blob", params }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement("a"); link.href = url; const epoch = Math.floor(Date.now() / 1000); link.setAttribute( "download", `ai-session-logs-${orgId}-${epoch}.csv` ); document.body.appendChild(link); link.click(); link.parentNode?.removeChild(link); } catch (error) { let apiErrorMessage: string | null = null; if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (data instanceof Blob && data.type === "application/json") { const text = await data.text(); const errorData = JSON.parse(text); apiErrorMessage = errorData.message; } } toast({ title: t("error"), description: apiErrorMessage ?? t("exportError"), variant: "destructive" }); } }; const columns: ColumnDef[] = [ { accessorKey: "createdAt", header: ({ column }) => ( {t("timestamp")} ), cell: ({ row }) => { return (
{new Date(row.original.createdAt).toLocaleString()}
); } }, { accessorKey: "providerName", header: ({ column }) => { return (
({ value: provider.id.toString(), label: provider.name || "Unnamed Provider" }) )} selectedValue={filters.providerId} onValueChange={(value) => handleFilterChange("providerId", value) } label={t("provider")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { return ( {row.original.providerName || "-"} ); } }, { accessorKey: "capability", header: ({ column }) => { return (
({ value, label }) )} selectedValue={filters.capability} onValueChange={(value) => handleFilterChange("capability", value) } label={t("capability")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { return ( {capabilityLabels[row.original.capability] || row.original.capability} ); } }, { accessorKey: "requestedModel", header: ({ column }) => { return (
({ value: model, label: model }))} selectedValue={filters.model} onValueChange={(value) => handleFilterChange("model", value) } label={t("model")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { return {row.original.requestedModel || "-"}; } }, { accessorKey: "resourceName", header: ({ column }) => { return (
({ value: res.id.toString(), label: res.name || "Unnamed Resource" }))} selectedValue={filters.resourceId} onValueChange={(value) => handleFilterChange("resourceId", value) } label={t("resource")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { if (!row.original.resourceNiceId) { return ( - ); } return ( e.stopPropagation()} > ); } }, { accessorKey: "isStream", header: ({ column }) => { return (
handleFilterChange("isStream", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { return ( {row.original.isStream ? ( <> {t("streaming")} ) : ( {t("nonStreaming")} )} ); } }, { accessorKey: "userEmail", header: ({ column }) => { return (
({ value: user.id, label: user.email || user.id }))} selectedValue={filters.actor} onValueChange={(value) => handleFilterChange("actor", value) } label={t("actor")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { return ( {row.original.userEmail ? ( <> {row.original.userEmail} ) : ( <>- )} ); } }, { accessorKey: "virtualApiKeyId", header: ({ column }) => { return (
({ value: key.id, label: key.name ?? (key.lastChars ? formatVirtualApiKeyPreview( key.id, key.lastChars ) : key.id) }) )} selectedValue={filters.virtualApiKeyId} onValueChange={(value) => handleFilterChange("virtualApiKeyId", value) } label={t("virtualApiKey")} searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} />
); }, cell: ({ row }) => { if (!row.original.virtualApiKeyId) { return ( - ); } return (
{row.original.virtualApiKeyName ?? t("aiUsageUnnamedVirtualApiKey")} {row.original.virtualApiKeyLastChars && ( {formatVirtualApiKeyPreview( row.original.virtualApiKeyId, row.original.virtualApiKeyLastChars )} )}
); } } ]; const renderExpandedRow = (row: any) => { return (
{t("aiSessionId")}

{row.sessionId}

{t("statusCode")}

{row.statusCode ?? "N/A"}

{t("cost")}

{row.usage && row.usage.costUsd != null ? `$${row.usage.costUsd.toFixed(4)}` : "N/A"}

{t("estimated")}

{row.usage ? row.usage.estimated ? t("yes") : t("no") : "N/A"}

{t("virtualApiKey")}

{row.virtualApiKeyId ? ( <> {row.virtualApiKeyName ?? t("aiUsageUnnamedVirtualApiKey")} {row.virtualApiKeyLastChars && ( <> {" "} ( {formatVirtualApiKeyPreview( row.virtualApiKeyId, row.virtualApiKeyLastChars )} ) )} ) : ( t("noVirtualApiKey") )}

{row.usage && (
{t("tokenUsage")}
{t("promptTokens")}

{row.usage.promptTokens.toLocaleString()}

{t("cacheReadTokens")}

{row.usage.cacheReadTokens.toLocaleString()}

{t("cacheWriteTokens")}

{row.usage.cacheWriteTokens.toLocaleString()}

{t("completionTokens")}

{row.usage.completionTokens.toLocaleString()}

{t("reasoningTokens")}

{row.usage.reasoningTokens.toLocaleString()}

{t("totalTokens")}

{row.usage.totalTokens.toLocaleString()}

)}
); }; return ( <> refetch()} isRefreshing={isFetching} onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} dateRange={{ start: dateRange.startDate, end: dateRange.endDate }} defaultSort={{ id: "createdAt", desc: true }} totalCount={totalCount} currentPage={currentPage} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} isLoading={isLoading} pageSize={pageSize} expandable={true} renderExpandedRow={renderExpandedRow} /> ); } function generateSampleAiSessionLogs(): QueryAiSessionLogResponse["log"] { const capabilities = Object.keys(capabilityLabels); const providers = [ { id: 1, name: "OpenAI Production" }, { id: 2, name: "Anthropic Default" }, { id: 3, name: "Vertex AI" } ]; const resourcesSample = [ { id: 1, niceId: "resource-1", name: "Resource 1" }, { id: 2, niceId: "resource-2", name: "Resource 2" } ]; const actors = ["alice@example.com", "bob@example.com", null]; const models = ["gpt-4o", "claude-sonnet-5", "gemini-2.5-pro"]; const virtualApiKeysSample = [ { id: "vak00001", name: "CI pipeline", lastChars: "ab12" }, { id: "vak00002", name: null, lastChars: "cd34" }, null ]; const now = Date.now(); const sevenDaysAgoMs = now - 7 * 24 * 60 * 60 * 1000; return Array.from({ length: 10 }, (_, i) => { const provider = providers[Math.floor(Math.random() * providers.length)]; const resource = resourcesSample[Math.floor(Math.random() * resourcesSample.length)]; const actor = actors[Math.floor(Math.random() * actors.length)]; const virtualApiKey = virtualApiKeysSample[ Math.floor(Math.random() * virtualApiKeysSample.length) ]; return { id: i, sessionId: `sample-session-${i}`, orgId: "sample-org", providerId: provider.id, providerName: provider.name, providerType: "openai", capability: capabilities[Math.floor(Math.random() * capabilities.length)], resourceId: resource.id, siteResourceId: null, resourceName: resource.name, resourceNiceId: resource.niceId, resourceType: "public", userId: actor ? `user-${i}` : null, userEmail: actor, virtualApiKeyId: virtualApiKey?.id ?? null, virtualApiKeyName: virtualApiKey?.name ?? null, virtualApiKeyLastChars: virtualApiKey?.lastChars ?? null, requestedModel: models[Math.floor(Math.random() * models.length)], isStream: Math.random() > 0.5, requestBody: null, responseBody: null, normalizedRequest: null, normalizedResponse: null, truncated: false, statusCode: 200, createdAt: Math.floor( sevenDaysAgoMs + Math.random() * (now - sevenDaysAgoMs) ), usage: { promptTokens: 500, cacheReadTokens: 0, cacheWriteTokens: 0, completionTokens: 150, reasoningTokens: 0, totalTokens: 650, costUsd: 0.0123, estimated: false } }; }); }