diff --git a/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx b/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx index 933b31a..7f0ce6a 100644 --- a/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx +++ b/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx @@ -1,24 +1,16 @@ import { Button, HStack, Icon, Text } from "@chakra-ui/react"; import { BsPlus } from "react-icons/bs"; -import { comparators } from "~/state/logFiltersSlice"; +import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice"; import { useAppStore } from "~/state/store"; -import { useFilterableFields } from "~/utils/hooks"; const AddFilterButton = () => { - const filterableFields = useFilterableFields().data; - const addFilter = useAppStore((s) => s.logFilters.addFilter); - if (!filterableFields || !filterableFields.length || !comparators || !comparators.length) - return null; - return ( - addFilter({ field: filterableFields[0] as string, comparator: comparators[0] }) - } + onClick={() => addFilter({ field: defaultFilterableFields[0], comparator: comparators[0] })} spacing={0} fontSize="sm" > diff --git a/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx b/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx index 0d286e0..5d217d3 100644 --- a/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx +++ b/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx @@ -1,10 +1,10 @@ -import { type LogFilter } from "~/state/logFiltersSlice"; +import { defaultFilterableFields, type LogFilter } from "~/state/logFiltersSlice"; import { useAppStore } from "~/state/store"; -import { useFilterableFields } from "~/utils/hooks"; +import { useTagNames } from "~/utils/hooks"; import InputDropdown from "~/components/InputDropdown"; const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: number }) => { - const filterableFields = useFilterableFields().data; + const tagNames = useTagNames().data; const updateFilter = useAppStore((s) => s.logFilters.updateFilter); @@ -12,7 +12,7 @@ const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: numb return ( updateFilter(index, { ...filter, field: option })} /> diff --git a/app/src/components/requestLogs/TableRow.tsx b/app/src/components/requestLogs/TableRow.tsx index 85db839..c4975cd 100644 --- a/app/src/components/requestLogs/TableRow.tsx +++ b/app/src/components/requestLogs/TableRow.tsx @@ -21,7 +21,7 @@ import Link from "next/link"; import { type RouterOutputs } from "~/utils/api"; import { FormattedJson } from "./FormattedJson"; import { useAppStore } from "~/state/store"; -import { useLoggedCalls } from "~/utils/hooks"; +import { useLoggedCalls, useTagNames } from "~/utils/hooks"; import { useMemo } from "react"; dayjs.extend(relativeTime); @@ -37,6 +37,7 @@ export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => { if (!matchingLogIds || !matchingLogIds.length) return false; return matchingLogIds.every((id) => selectedLogIds.has(id)); }, [selectedLogIds, matchingLogIds]); + const tagNames = useTagNames().data; return ( @@ -58,6 +59,7 @@ export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => { )} Sent At Model + {tagNames?.map((tagName) => {tagName})} Duration Input tokens Output tokens @@ -82,19 +84,11 @@ export const TableRow = ({ const requestedAt = dayjs(loggedCall.requestedAt).format("MMMM D h:mm A"); const fullTime = dayjs(loggedCall.requestedAt).toString(); - const durationCell = ( - - {loggedCall.cacheHit ? ( - Cached - ) : ( - ((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s" - )} - - ); - const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id)); const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId); + const tagNames = useTagNames().data; + return ( <> - + - {durationCell} + {tagNames?.map((tagName) => {loggedCall.tags[tagName]})} + + {loggedCall.cacheHit ? ( + Cached + ) : ( + ((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s" + )} + {loggedCall.modelResponse?.inputTokens} {loggedCall.modelResponse?.outputTokens} diff --git a/app/src/server/api/routers/loggedCalls.router.ts b/app/src/server/api/routers/loggedCalls.router.ts index 9b0efd1..a837b5c 100644 --- a/app/src/server/api/routers/loggedCalls.router.ts +++ b/app/src/server/api/routers/loggedCalls.router.ts @@ -1,14 +1,12 @@ import { z } from "zod"; -import { type Expression, type SqlBool } from "kysely"; -import { sql } from "kysely"; +import { type Expression, type SqlBool, sql } from "kysely"; +import { jsonArrayFrom } from "kysely/helpers/postgres"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { kysely, prisma } from "~/server/db"; -import { comparators } from "~/state/logFiltersSlice"; +import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice"; import { requireCanViewProject } from "~/utils/accessControl"; -const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"]; - // create comparator type based off of comparators const comparatorToSqlValue = (comparator: (typeof comparators)[number], value: string) => { switch (comparator) { @@ -91,7 +89,10 @@ export const loggedCallsRouter = createTRPCRouter({ }); const tagFilters = input.filters.filter( - (filter) => !defaultFilterableFields.includes(filter.field), + (filter) => + !defaultFilterableFields.includes( + filter.field as (typeof defaultFilterableFields)[number], + ), ); let updatedBaseQuery = baseQuery; @@ -112,7 +113,7 @@ export const loggedCallsRouter = createTRPCRouter({ } const rawCalls = await updatedBaseQuery - .select([ + .select((eb) => [ "lc.id as id", "lc.requestedAt as requestedAt", "model", @@ -127,28 +128,45 @@ export const loggedCallsRouter = createTRPCRouter({ "cost", "statusCode", "durationMs", + jsonArrayFrom( + eb + .selectFrom("LoggedCallTag") + .select(["name", "value"]) + .whereRef("loggedCallId", "=", "lc.id"), + ).as("tags"), ]) .orderBy("lc.requestedAt", "desc") .limit(pageSize) .offset((page - 1) * pageSize) .execute(); - const calls = rawCalls.map((rawCall) => ({ - id: rawCall.id, - requestedAt: rawCall.requestedAt, - model: rawCall.model, - cacheHit: rawCall.cacheHit, - modelResponse: { - receivedAt: rawCall.receivedAt, - reqPayload: rawCall.reqPayload, - respPayload: rawCall.respPayload, - inputTokens: rawCall.inputTokens, - outputTokens: rawCall.outputTokens, - cost: rawCall.cost, - statusCode: rawCall.statusCode, - durationMs: rawCall.durationMs, - }, - })); + const calls = rawCalls.map((rawCall) => { + const tagsObject = rawCall.tags.reduce( + (acc, tag) => { + acc[tag.name] = tag.value; + return acc; + }, + {} as Record, + ); + + return { + id: rawCall.id, + requestedAt: rawCall.requestedAt, + model: rawCall.model, + cacheHit: rawCall.cacheHit, + modelResponse: { + receivedAt: rawCall.receivedAt, + reqPayload: rawCall.reqPayload, + respPayload: rawCall.respPayload, + inputTokens: rawCall.inputTokens, + outputTokens: rawCall.outputTokens, + cost: rawCall.cost, + statusCode: rawCall.statusCode, + durationMs: rawCall.durationMs, + }, + tags: tagsObject, + }; + }); const matchingLogIds = await updatedBaseQuery.select(["lc.id"]).execute(); @@ -156,7 +174,7 @@ export const loggedCallsRouter = createTRPCRouter({ return { calls, count, matchingLogIds: matchingLogIds.map((log) => log.id) }; }), - getFilterableFields: protectedProcedure + getTagNames: protectedProcedure .input(z.object({ projectId: z.string() })) .query(async ({ input, ctx }) => { await requireCanViewProject(input.projectId, ctx); @@ -169,8 +187,11 @@ export const loggedCallsRouter = createTRPCRouter({ select: { name: true, }, + orderBy: { + name: "asc", + }, }); - return [...defaultFilterableFields, ...tags.map((tag) => tag.name)]; + return tags.map((tag) => tag.name); }), }); diff --git a/app/src/state/logFiltersSlice.ts b/app/src/state/logFiltersSlice.ts index 4c4a937..1af0f21 100644 --- a/app/src/state/logFiltersSlice.ts +++ b/app/src/state/logFiltersSlice.ts @@ -1,9 +1,9 @@ import { type SliceCreator } from "./store"; -export const editorBackground = "#fafafa"; - export const comparators = ["=", "!=", "CONTAINS"] as const; +export const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"] as const; + export interface LogFilter { field: string; comparator: (typeof comparators)[number]; diff --git a/app/src/state/selectedLogsSlice.ts b/app/src/state/selectedLogsSlice.ts index 43ca247..1af55c1 100644 --- a/app/src/state/selectedLogsSlice.ts +++ b/app/src/state/selectedLogsSlice.ts @@ -1,7 +1,5 @@ import { type SliceCreator } from "./store"; -export const editorBackground = "#fafafa"; - export type SelectedLogsSlice = { selectedLogIds: Set; toggleSelectedLogId: (id: string) => void; diff --git a/app/src/utils/hooks.ts b/app/src/utils/hooks.ts index 26f22b3..f3b715c 100644 --- a/app/src/utils/hooks.ts +++ b/app/src/utils/hooks.ts @@ -187,9 +187,9 @@ export const useLoggedCalls = () => { ); }; -export const useFilterableFields = () => { +export const useTagNames = () => { const selectedProjectId = useAppStore((state) => state.selectedProjectId); - return api.loggedCalls.getFilterableFields.useQuery( + return api.loggedCalls.getTagNames.useQuery( { projectId: selectedProjectId ?? "" }, { enabled: !!selectedProjectId }, );