diff --git a/.github/workflows/test-build-deploy.yml b/.github/workflows/test-build-deploy.yml index 91ef6c8c..206757b8 100644 --- a/.github/workflows/test-build-deploy.yml +++ b/.github/workflows/test-build-deploy.yml @@ -7,6 +7,24 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + name: List the state of node modules + continue-on-error: true + run: npm list - name: Install dependencies run: npm ci && npx playwright install --with-deps - name: Start backend @@ -18,7 +36,7 @@ jobs: LUNARY_PUBLIC_KEY: 259d2d94-9446-478a-ae04-484de705b522 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} timeout-minutes: 1 - run: npm run start:backend & npx wait-on http://localhost:3333/v1/health + run: npm run start:backend & npx wait-on http://localhost:3333/v1/health - name: Start frontend env: diff --git a/packages/backend/src/api/v1/auth/index.ts b/packages/backend/src/api/v1/auth/index.ts index d007eda2..a7aeb817 100644 --- a/packages/backend/src/api/v1/auth/index.ts +++ b/packages/backend/src/api/v1/auth/index.ts @@ -7,6 +7,7 @@ import Router from "koa-router" import { z } from "zod" import { hashPassword, + requestPasswordReset, sanitizeEmail, signJwt, verifyJwt, @@ -235,8 +236,13 @@ auth.post("/login", async (ctx: Context) => { select * from account where email = ${email} ` if (!user) { - ctx.status = 403 - ctx.body = { error: "Unauthorized", message: "Invalid email or password" } + ctx.body = ctx.throw(403, "Invalid email or password") + } + + if (!user.passwordHash) { + await requestPasswordReset(email) + + ctx.body = { message: "We sent you an email to reset your password" } return } @@ -274,16 +280,8 @@ auth.post("/request-password-reset", async (ctx: Context) => { } const { email } = body.data - const [user] = await sql`select id from account where email = ${email}` - - const ONE_HOUR = 60 * 60 - const token = await signJwt({ email }, ONE_HOUR) - - await sql`update account set recovery_token = ${token} where id = ${user.id}` - - const link = `${process.env.APP_URL}/reset-password?token=${token}` - await sendEmail(RESET_PASSWORD(email, link)) + await requestPasswordReset(email) ctx.body = { ok: true } } catch (error) { diff --git a/packages/backend/src/api/v1/auth/utils.ts b/packages/backend/src/api/v1/auth/utils.ts index 0174e6eb..99ef4160 100644 --- a/packages/backend/src/api/v1/auth/utils.ts +++ b/packages/backend/src/api/v1/auth/utils.ts @@ -7,6 +7,8 @@ import * as argon2 from "argon2" import bcrypt from "bcrypt" import { validateUUID } from "@/src/utils/misc" +import { sendEmail } from "@/src/utils/sendEmail" +import { RESET_PASSWORD } from "@/src/utils/emails" export function sanitizeEmail(email: string) { return email.toLowerCase().trim() @@ -132,3 +134,16 @@ export async function authMiddleware(ctx: Context, next: Next) { await next() } + +export async function requestPasswordReset(email: string) { + const [user] = await sql`select id from account where email = ${email}` + + const ONE_HOUR = 60 * 60 + const token = await signJwt({ email }, ONE_HOUR) + + await sql`update account set recovery_token = ${token} where id = ${user.id}` + + const link = `${process.env.APP_URL}/reset-password?token=${token}` + + await sendEmail(RESET_PASSWORD(email, link)) +} diff --git a/packages/backend/src/api/v1/evaluations/index.ts b/packages/backend/src/api/v1/evaluations/index.ts index 7811c134..e04001d9 100644 --- a/packages/backend/src/api/v1/evaluations/index.ts +++ b/packages/backend/src/api/v1/evaluations/index.ts @@ -81,7 +81,6 @@ evaluations.post( queue.on("active", () => { const percentDone = ((count - queue.size) / count) * 100 console.log(`Active: ${queue.size} of ${count} (${percentDone}%)`) - console.log() stream.write(JSON.stringify({ percentDone }) + "\n") }) diff --git a/packages/backend/src/api/v1/external-users.ts b/packages/backend/src/api/v1/external-users.ts index d5cc766d..122f2855 100644 --- a/packages/backend/src/api/v1/external-users.ts +++ b/packages/backend/src/api/v1/external-users.ts @@ -10,11 +10,12 @@ const users = new Router({ users.get("/", checkAccess("users", "list"), async (ctx: Context) => { const { projectId } = ctx.state - // const { limit, page } = ctx.query + const { limit = "100", page = "0", search } = ctx.query - // if(!limit || !page) { - // return ctx.throw(400, "limit and page are required") - // } + let searchQuery = sql`` + if (search) { + searchQuery = sql`and (lower(external_id) ilike lower(${`%${search}%`}) or lower(props->>'email') ilike lower(${`%${search}%`}) or lower(props->>'name') ilike lower(${`%${search}%`}))` + } // TODO: pagination const users = await sql` @@ -30,9 +31,11 @@ users.get("/", checkAccess("users", "list"), async (ctx: Context) => { external_user where project_id = ${projectId} + ${searchQuery} order by - cost desc - ` + cost desc + limit ${Number(limit)} + offset ${Number(page) * Number(limit)}` ctx.body = users }) diff --git a/packages/backend/src/api/v1/filters.ts b/packages/backend/src/api/v1/filters.ts index c42529fe..68576ff8 100644 --- a/packages/backend/src/api/v1/filters.ts +++ b/packages/backend/src/api/v1/filters.ts @@ -1,7 +1,6 @@ import sql from "@/src/utils/db" import Router from "koa-router" import { Context } from "koa" -// import { checkAccess } from "@/src/utils/authorization" const filters = new Router({ prefix: "/filters", @@ -17,8 +16,6 @@ filters.get("/models", async (ctx: Context) => { model_name_cache where project_id = ${projectId} - order by - project_id ` ctx.body = rows.map((row) => row.name) @@ -34,8 +31,6 @@ filters.get("/tags", async (ctx: Context) => { tag_cache where project_id = ${projectId} - order by - project_id ` ctx.body = rows.map((row) => row.tag) @@ -43,49 +38,17 @@ filters.get("/tags", async (ctx: Context) => { filters.get("/feedback", async (ctx: Context) => { const { projectId } = ctx.state - const { type } = ctx.query const rows = await sql` select - jsonb_build_object ('thumbs', - feedback::json ->> 'thumbs') - from - run - where - feedback::json ->> 'thumbs' is not null - and project_id = ${projectId} - union - select - jsonb_build_object ('emoji', - feedback::json ->> 'emoji') - from - run - where - feedback::json ->> 'emoji' is not null - and project_id = ${projectId} - union - select - jsonb_build_object ('rating', - CAST(feedback::json ->> 'rating' as INT)) - from - run - where - feedback::json ->> 'rating' is not null - and project_id = ${projectId} - union - select - jsonb_build_object ('retried', - CAST(feedback::json ->> 'retried' as boolean)) + feedback from - run + feedback_cache where - feedback::json ->> 'retried' is not null - and project_id = ${projectId} + project_id = ${projectId} ` - const feedbacks = rows.map((row) => row.jsonbBuildObject) - - ctx.body = feedbacks + ctx.body = rows.map((row) => row.feedback) // stringify so it works with selected }) // get all unique keys in metadata table @@ -101,8 +64,6 @@ filters.get("/metadata", async (ctx: Context) => { metadata_cache where project_id = ${projectId} - order by - project_id ` ctx.body = rows.map((row) => row.key) @@ -114,14 +75,11 @@ filters.get("/users", async (ctx) => { const rows = await sql` select - external_id as label, - id as value + * from external_user where project_id = ${projectId} - order by - project_id ` ctx.body = rows @@ -138,8 +96,6 @@ filters.get("/radars", async (ctx) => { radar where project_id = ${projectId} - order by - project_id ` ctx.body = rows diff --git a/packages/backend/src/api/v1/runs/index.ts b/packages/backend/src/api/v1/runs/index.ts index e6ac53c1..e45121ef 100644 --- a/packages/backend/src/api/v1/runs/index.ts +++ b/packages/backend/src/api/v1/runs/index.ts @@ -59,6 +59,8 @@ const formatRun = (run: any) => ({ id: run.id, isPublic: run.isPublic, feedback: run.feedback, + parentFeedback: run.parentFeedback, + type: run.type, name: run.name, createdAt: run.createdAt, @@ -120,10 +122,12 @@ runs.get("/", async (ctx: Context) => { eu.external_id as user_external_id, eu.created_at as user_created_at, eu.last_seen as user_last_seen, - eu.props as user_props + eu.props as user_props, + rpfc.feedback as parent_feedback from run r left join external_user eu on r.external_user_id = eu.id + left join run_parent_feedback_cache rpfc ON r.id = rpfc.id where r.project_id = ${projectId} ${parentRunCheck} @@ -286,7 +290,8 @@ runs.get("/:id/related", checkAccess("logs", "read"), async (ctx) => { ctx.body = related }) -runs.get("/:id/feedback", checkAccess("logs", "read"), async (ctx) => { +// public route +runs.get("/:id/feedback", async (ctx) => { const { projectId } = ctx.state const { id } = ctx.params diff --git a/packages/backend/src/api/v1/runs/ingest.ts b/packages/backend/src/api/v1/runs/ingest.ts index 804a5ca2..92177812 100644 --- a/packages/backend/src/api/v1/runs/ingest.ts +++ b/packages/backend/src/api/v1/runs/ingest.ts @@ -184,7 +184,7 @@ const registerRunEvent = async ( SET feedback = ${sql.json({ ...((feedbackData?.feedback || {}) as any), ...feedback, - ...extra, + ...extra, // legacy })} WHERE id = ${runId} ` diff --git a/packages/backend/src/checks/index.ts b/packages/backend/src/checks/index.ts index cea22d92..7cc35c35 100644 --- a/packages/backend/src/checks/index.ts +++ b/packages/backend/src/checks/index.ts @@ -7,6 +7,7 @@ import aiSimilarity from "./ai/similarity" // import aiNER from "./ai/ner" // import aiToxicity from "./ai/toxic" import rouge from "rouge" +import { or } from "../utils/checks" function getTextsTypes(field: "any" | "input" | "output", run: any) { let textsToCheck = [] @@ -111,6 +112,27 @@ export const CHECK_RUNNERS: CheckRunner[] = [ id: "users", sql: ({ users }) => sql`external_user_id = ANY (${users})`, }, + { + id: "feedback", + sql: ({ types }) => { + // If one of the type is {"comment": ""}, we just need to check if there is a 'comment' key + // otherwise, we need to check for the key:value pair + + return or( + types.map((type: string) => { + const parsedType = JSON.parse(type) + const key = Object.keys(parsedType)[0] + const value = parsedType[key] + if (key === "comment") { + // comment is a special case because there can be infinite values + return sql`r.feedback->${key} IS NOT NULL OR rpfc.feedback->${key} IS NOT NULL` + } else { + return sql`r.feedback->>${key} = ${value} OR rpfc.feedback->>${key} = ${value}` + } + }), + ) + }, + }, { id: "regex", evaluator: async (run, params) => { diff --git a/packages/backend/src/utils/checks.ts b/packages/backend/src/utils/checks.ts index 1451f319..09ea12b3 100644 --- a/packages/backend/src/utils/checks.ts +++ b/packages/backend/src/utils/checks.ts @@ -2,9 +2,9 @@ import { CheckLogic, LogicElement } from "shared" import sql from "./db" import CHECK_RUNNERS from "../checks" -const and = (arr: any = []) => +export const and = (arr: any = []) => arr.reduce((acc: any, x: any) => sql`${acc} AND ${x}`) -const or = (arr: any = []) => +export const or = (arr: any = []) => arr.reduce((acc: any, x: any) => sql`(${acc} OR ${x})`) // TODO: unit tests diff --git a/packages/backend/src/utils/cron.ts b/packages/backend/src/utils/cron.ts index 754dc4b1..562334b6 100644 --- a/packages/backend/src/utils/cron.ts +++ b/packages/backend/src/utils/cron.ts @@ -13,6 +13,7 @@ export function setupCronJobs() { "tag_cache", "metadata_cache", "feedback_cache", + "run_parent_feedback_cache", ] try { diff --git a/packages/backend/src/utils/emails.ts b/packages/backend/src/utils/emails.ts index 413992a0..f153de68 100644 --- a/packages/backend/src/utils/emails.ts +++ b/packages/backend/src/utils/emails.ts @@ -72,6 +72,7 @@ export function RESET_PASSWORD(email: string, confirmLink: string) { text: `Hi, Please click on the link below to reset your password: + ${confirmLink} You can reply to this email if you have any question. diff --git a/packages/backend/src/utils/errors.ts b/packages/backend/src/utils/errors.ts index ee2f54d4..e3b4bf95 100644 --- a/packages/backend/src/utils/errors.ts +++ b/packages/backend/src/utils/errors.ts @@ -13,9 +13,7 @@ export async function errorMiddleware(ctx: Context, next: Next) { console.error(error) sendErrorToSentry(error, ctx) - console.log(error) if (error instanceof z.ZodError) { - console.log("HERE") ctx.status = 422 ctx.body = { error: "Error", diff --git a/packages/db/0008.sql b/packages/db/0008.sql new file mode 100644 index 00000000..06e9540b --- /dev/null +++ b/packages/db/0008.sql @@ -0,0 +1,100 @@ +DROP MATERIALIZED VIEW IF EXISTS feedback_cache; + +create materialized view feedback_cache as +select + run.project_id, + jsonb_build_object ('thumbs', + feedback::json ->> 'thumbs') as feedback +from + run +where + feedback::json ->> 'thumbs' is not null +group by + run.project_id, + feedback::json ->> 'thumbs' +union +select + run.project_id, + jsonb_build_object ('emoji', + feedback::json ->> 'emoji') as feedback +from + run +where + feedback::json ->> 'emoji' is not null +group by + run.project_id, + feedback::json ->> 'emoji' +union +select + run.project_id, + jsonb_build_object ('rating', + CAST(feedback::json ->> 'rating' as INT)) as feedback +from + run +where + feedback::json ->> 'rating' is not null +group by + run.project_id, + CAST(feedback::json ->> 'rating' as INT) +union +select + run.project_id, + jsonb_build_object ('retried', + CAST(feedback::json ->> 'retried' as boolean)) as feedback +from + run +where + feedback::json ->> 'retried' is not null +group by + run.project_id, + CAST(feedback::json ->> 'retried' as boolean) +union +select + run.project_id, + jsonb_build_object ('comment', '') as feedback +from + run +where + feedback::json ->> 'comment' is not null +group by + run.project_id, + feedback::json ->> 'comment'; + +create unique index on feedback_cache (project_id, feedback); +create index on feedback_cache(project_id); + +create index if not exists idx_run_id_parent_run_id on run (id, parent_run_id); +create index if not exists idx_run_feedback_null ON run (id, parent_run_id) WHERE feedback IS NULL; +create index if not exists idx_run_parent_run_id_feedback ON run (parent_run_id, feedback); +CREATE INDEX if not exists idx_run_id_parent_run_id_feedback ON run (id, parent_run_id, feedback); + +create materialized view run_parent_feedback_cache as +WITH RECURSIVE run_feedback AS ( + SELECT + r.id, + r.parent_run_id, + r.feedback, + 1 AS depth + FROM + run r + UNION ALL + SELECT + rf.id, + r.parent_run_id, + COALESCE(r.feedback, rf.feedback), + rf.depth + 1 + FROM + run_feedback rf + JOIN run r ON rf.parent_run_id = r.id + WHERE + rf.depth < 5 AND rf.feedback IS NULL +) +SELECT + id, + feedback +FROM + run_feedback +WHERE + feedback IS NOT NULL; + +create unique index on run_parent_feedback_cache(id); diff --git a/packages/frontend/components/blocks/DataTable.tsx b/packages/frontend/components/blocks/DataTable.tsx index f7e23655..bfa16fc3 100644 --- a/packages/frontend/components/blocks/DataTable.tsx +++ b/packages/frontend/components/blocks/DataTable.tsx @@ -125,7 +125,12 @@ export default function DataTable({ table.getAllColumns().forEach((column) => { if (!autoHidableColumns.includes(column.id)) return - const isUsed = rows.some((row) => row.original[column.id]) + const isUsed = rows.some( + (row) => + row.original[column.id] || + // Special case with feedback column which is sometimes in parentFeedback + (column.id === "feedback" && row.original.parentFeedback), + ) column.toggleVisibility(isUsed) }) diff --git a/packages/frontend/components/blocks/Feedback.tsx b/packages/frontend/components/blocks/Feedback.tsx index ca1e5076..4961d6d0 100644 --- a/packages/frontend/components/blocks/Feedback.tsx +++ b/packages/frontend/components/blocks/Feedback.tsx @@ -1,4 +1,4 @@ -import { Group, Tooltip } from "@mantine/core" +import { Group, Indicator, Tooltip } from "@mantine/core" import { IconMessage, @@ -7,11 +7,17 @@ import { IconThumbDown, IconThumbUp, } from "@tabler/icons-react" -import { useEffect } from "react" +import { useEffect, useState } from "react" import analytics from "../../utils/analytics" import { useFixedColorScheme } from "@/utils/hooks" -export default function Feedback({ data = {} }: { data: Record }) { +export default function Feedback({ + data = {}, + isFromParent, +}: { + data: Record + isFromParent?: boolean +}) { const scheme = useFixedColorScheme() useEffect(() => { // Feature tracking @@ -28,25 +34,41 @@ export default function Feedback({ data = {} }: { data: Record }) { }) return ( - - {data?.thumbs === "up" && } - {data?.thumbs === "down" && } - {typeof data?.rating === "number" && ( - - {Array.from({ length: data.rating }).map((_, i) => ( - - ))} + + + {/* */} + + {data?.thumbs === "up" && } + {data?.thumbs === "down" && ( + + )} + {typeof data?.rating === "number" && ( + + {Array.from({ length: data.rating }).map((_, i) => ( + + ))} + + )} + {data?.emoji && {data.emoji}} + {typeof data?.comment === "string" && ( + /* Support for comment == "" in the filters */ + + + + )} + {data?.retried && ( + + )} - )} - {data?.emoji && {data.emoji}} - {data?.comment && ( - - - - )} - {data?.retried && ( - - )} - + + ) } diff --git a/packages/frontend/components/checks/AddCheck.tsx b/packages/frontend/components/checks/AddCheck.tsx index 9367a193..418855ff 100644 --- a/packages/frontend/components/checks/AddCheck.tsx +++ b/packages/frontend/components/checks/AddCheck.tsx @@ -6,7 +6,7 @@ import { ScrollArea, useCombobox, } from "@mantine/core" -import CHECKS_UI_DATA from "./UIData" +import CHECKS_UI_DATA from "./ChecksUIData" import { IconPlus } from "@tabler/icons-react" export function AddCheckButton({ checks, onSelect, defaultOpened }) { diff --git a/packages/frontend/components/checks/ChecksInputs.tsx b/packages/frontend/components/checks/ChecksInputs.tsx index 9ba57d8f..df45eb6e 100644 --- a/packages/frontend/components/checks/ChecksInputs.tsx +++ b/packages/frontend/components/checks/ChecksInputs.tsx @@ -1,60 +1,10 @@ -import { - Flex, - MultiSelect, - NumberInput, - Select, - Text, - TextInput, -} from "@mantine/core" +import { Flex, NumberInput, Text, TextInput } from "@mantine/core" import classes from "./index.module.css" -import { useProjectSWR } from "@/utils/dataHooks" +import SmartCheckSelect from "./SmartSelectInput" const CheckInputs = { - select: ({ - options, - placeholder, - width, - render, - multiple, - value, - onChange, - }) => { - const useSWRforData = typeof options === "function" + select: SmartCheckSelect, - const { data: swrCheckData } = useProjectSWR( - useSWRforData ? options() : null, - ) - - const data = useSWRforData ? swrCheckData : options - - const Component = multiple ? MultiSelect : Select - - const isDataObject = data && typeof data[0] === "object" - - return data ? ( - { - return { - value: `${d.value}`, // stringify to avoid issues with numbers - label: render ? render(d) : d.label, - } - }) - : data?.filter((d) => Boolean(d) === true) - } - /> - ) : ( - "loading..." - ) - }, number: ({ placeholder, width, min, max, step, value, onChange, unit }) => { return ( diff --git a/packages/frontend/components/checks/ChecksModal.tsx b/packages/frontend/components/checks/ChecksModal.tsx index 34f1be4b..c949f75b 100644 --- a/packages/frontend/components/checks/ChecksModal.tsx +++ b/packages/frontend/components/checks/ChecksModal.tsx @@ -18,7 +18,7 @@ import { import { IconCircleCheck, IconCirclePlus } from "@tabler/icons-react" import classes from "./index.module.css" import { CHECKS, Check } from "shared" -import CHECKS_UI_DATA from "./UIData" +import CHECKS_UI_DATA from "./ChecksUIData" import { useMemo, useState } from "react" function CheckCard({ diff --git a/packages/frontend/components/checks/UIData.tsx b/packages/frontend/components/checks/ChecksUIData.tsx similarity index 71% rename from packages/frontend/components/checks/UIData.tsx rename to packages/frontend/components/checks/ChecksUIData.tsx index 4ae67240..7eb94a35 100644 --- a/packages/frontend/components/checks/UIData.tsx +++ b/packages/frontend/components/checks/ChecksUIData.tsx @@ -1,6 +1,7 @@ import { IconAt, IconBiohazard, + IconBraces, IconBracketsContainStart, IconBrandOpenai, IconCalendar, @@ -32,12 +33,27 @@ import { IconUserCheck, IconWorldWww, } from "@tabler/icons-react" +import Feedback from "../blocks/Feedback" +import AppUserAvatar from "../blocks/AppUserAvatar" +import { Group, Text } from "@mantine/core" +import { capitalize, formatAppUser } from "@/utils/format" -const CHECKS_UI_DATA = { +type CheckUI = { + icon: React.FC + color: string + renderListItem?: (value: any) => JSX.Element + renderLabel?: (value: any) => JSX.Element + getItemValue?: (value: any) => string +} + +type ChecksUIData = { + [key: string]: CheckUI +} + +const CHECKS_UI_DATA: ChecksUIData = { model: { icon: IconBrandOpenai, color: "violet", - description: "Is the run.model in the list of model names", }, tags: { icon: IconTag, @@ -46,6 +62,28 @@ const CHECKS_UI_DATA = { users: { icon: IconUser, color: "blue", + renderListItem: (item) => ( + <> + + {formatAppUser(item)} + + ), + renderLabel: (item) => formatAppUser(item), + }, + feedback: { + icon: IconThumbUp, + color: "green", + renderListItem: (item) => { + const key = Object.keys(item)[0] + const value = item[key] || "" + return ( + <> + + {`${capitalize(key)}${value ? ": " + value : ""}`} + + ) + }, + renderLabel: (value) => , }, date: { icon: IconCalendar, @@ -67,10 +105,7 @@ const CHECKS_UI_DATA = { icon: IconCoin, color: "pink", }, - feedback: { - icon: IconThumbUp, - color: "green", - }, + json: { icon: IconJson, color: "violet", @@ -119,6 +154,10 @@ const CHECKS_UI_DATA = { icon: IconHelpCircle, color: "blue", }, + metadata: { + icon: IconBraces, + color: "blue", + }, hatred: { icon: IconMoodAngry, color: "red", diff --git a/packages/frontend/components/checks/Picker.tsx b/packages/frontend/components/checks/Picker.tsx index d88d3ac1..b6f20561 100644 --- a/packages/frontend/components/checks/Picker.tsx +++ b/packages/frontend/components/checks/Picker.tsx @@ -1,5 +1,5 @@ -import { Box, Button, Group, Select, Stack, Text } from "@mantine/core" -import { Fragment, useCallback, useEffect, useState } from "react" +import { Box, Button, Group, Select, Stack } from "@mantine/core" +import { Fragment, useState } from "react" import { CHECKS, Check, CheckLogic, CheckParam, LogicData } from "shared" import ErrorBoundary from "../blocks/ErrorBoundary" import { AddCheckButton } from "./AddCheck" @@ -7,6 +7,7 @@ import CheckInputs from "./ChecksInputs" import ChecksModal from "./ChecksModal" import classes from "./index.module.css" import { IconX } from "@tabler/icons-react" +import CHECKS_UI_DATA from "./ChecksUIData" function RenderCheckNode({ minimal, @@ -93,6 +94,8 @@ function RenderCheckNode({ const paramData = isParamNotLabel ? s.params[param.id] : null + const UIItem = CHECKS_UI_DATA[check.id] || CHECKS_UI_DATA["other"] + const width = isParamNotLabel && param.width ? minimal @@ -116,6 +119,9 @@ function RenderCheckNode({ (typeof item === "object" ? item?.label : item), + getItemValue = (item) => (typeof item === "object" ? `${item.value}` : item), + value, + onChange, +}) { + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption(), + onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"), + }) + + const [search, setSearch] = useState("") + + const useSWRforData = typeof options === "function" + const { data: swrCheckData } = useProjectSWR(useSWRforData ? options() : null) + const data = useSWRforData ? swrCheckData : options + + const fixedValue = value || (multiple ? [] : null) + + const handleValueSelect = (val: string) => { + return multiple + ? onChange( + fixedValue.includes(val) + ? fixedValue.filter((v) => v !== val) + : [...fixedValue, val], + ) + : onChange(val) + } + + const handleValueRemove = (val: string) => { + return multiple + ? onChange(fixedValue.filter((v) => v !== val)) + : onChange(null) + } + + const renderedValue = multiple + ? fixedValue.map((item) => ( + handleValueRemove(item)} + > + {renderLabel(data?.find((d) => getItemValue(d) === item))} + + )) + : renderLabel(data?.find((d) => getItemValue(d) === value)) + + const renderedOptions = data + ?.filter((item) => + search.length === 0 + ? true + : customSearch + ? customSearch(search, item) + : getItemValue(item) + .toLowerCase() + .includes(search.trim().toLowerCase()), + ) + .map((item) => ( + + + {value?.includes(getItemValue(item)) ? : null} + {renderListItem ? renderListItem(item) : renderLabel(item)} + + + )) + + useEffect(() => { + if (!value) { + combobox.openDropdown() + } + }, []) + + return ( + + + combobox.openDropdown()} + variant="unstyled" + miw={width} + w="min-content" + > + + {renderedValue} + + {(!renderedValue || !renderedValue?.length || searchable) && ( + + combobox.openDropdown()} + onBlur={() => combobox.closeDropdown()} + value={search} + w={80} + placeholder={placeholder} + onChange={(event) => { + combobox.updateSelectedOptionIndex() + setSearch(event.currentTarget.value) + }} + onKeyDown={(event) => { + if (event.key === "Backspace" && search.length === 0) { + event.preventDefault() + handleValueRemove(value[value.length - 1]) + } + }} + /> + + )} + + + + + + {data?.length > 5 && searchable && ( + setSearch(event.currentTarget.value)} + placeholder={"Search..."} + /> + )} + + {renderedOptions?.length > 0 ? ( + renderedOptions + ) : ( + Nothing found... + )} + + + + ) +} diff --git a/packages/frontend/components/layout/Navbar.tsx b/packages/frontend/components/layout/Navbar.tsx index 5ee57d8c..b196fca8 100644 --- a/packages/frontend/components/layout/Navbar.tsx +++ b/packages/frontend/components/layout/Navbar.tsx @@ -57,19 +57,16 @@ export default function Navbar() { setSendingEmail(true) const ok = await errorHandler( - fetch( - `${process.env.NEXT_PUBLIC_API_URL || window.location.origin}/v1/users/send-verification`, - { - method: "POST", - body: JSON.stringify({ - email: user?.email, - name: user?.name, - }), - headers: { - "Content-Type": "application/json", - }, + fetch(`${process.env.API_URL}/v1/users/send-verification`, { + method: "POST", + body: JSON.stringify({ + email: user?.email, + name: user?.name, + }), + headers: { + "Content-Type": "application/json", }, - ), + }), ) if (ok) { diff --git a/packages/frontend/pages/join.tsx b/packages/frontend/pages/join.tsx index d2c3aa58..54337c70 100644 --- a/packages/frontend/pages/join.tsx +++ b/packages/frontend/pages/join.tsx @@ -119,8 +119,6 @@ export default function Join() { redirectUrl, } - console.log(signupData) - const ok = await errorHandler( fetcher.post("/auth/signup", { arg: signupData, @@ -136,11 +134,7 @@ export default function Join() { message: `You have joined ${orgName}`, }) - if (redirectUrl) { - window.location.href = redirectUrl - } else { - Router.replace("/login") - } + window.location.href = redirectUrl || "/login" } setLoading(false) @@ -158,8 +152,6 @@ export default function Join() { }, }) - console.log(method, redirect) - if (method === "saml") { setSsoURI(redirect) @@ -168,6 +160,8 @@ export default function Join() { name, redirectUrl: redirect, }) + } else { + setStep(2) } } else if (step === 2) { await handleSignup({ @@ -207,7 +201,7 @@ export default function Join() {
- + {step < 3 && ( <> Sign Up + + {step === "password" && ( + + Forgot password? + + )} diff --git a/packages/frontend/pages/logs/index.tsx b/packages/frontend/pages/logs/index.tsx index d8caf919..cd4fcdc1 100644 --- a/packages/frontend/pages/logs/index.tsx +++ b/packages/frontend/pages/logs/index.tsx @@ -24,6 +24,7 @@ import { timeColumn, userColumn, } from "@/utils/datatable" + import { IconBraces, IconBrandOpenai, @@ -33,6 +34,7 @@ import { IconMessages, IconFilter, } from "@tabler/icons-react" + import { NextSeo } from "next-seo" import { useContext, useEffect, useState } from "react" @@ -40,16 +42,17 @@ import { ChatReplay } from "@/components/blocks/RunChat" import RunInputOutput from "@/components/blocks/RunInputOutput" import SearchBar from "@/components/blocks/SearchBar" import { openUpgrade } from "@/components/layout/UpgradeModal" +import CheckPicker from "@/components/checks/Picker" +import Empty from "@/components/layout/Empty" + import analytics from "@/utils/analytics" import { formatDateTime } from "@/utils/format" +import { fetcher } from "@/utils/fetcher" import { useProject, useOrg, useProjectInfiniteSWR } from "@/utils/dataHooks" import { useDebouncedState, useDidUpdate } from "@mantine/hooks" import Router from "next/router" -import Empty from "../../components/layout/Empty" -import { ProjectContext } from "../../utils/context" -import CheckPicker from "@/components/checks/Picker" +import { ProjectContext } from "@/utils/context" import { CheckLogic, deserializeLogic, serializeLogic } from "shared" -import { fetcher } from "@/utils/fetcher" const columns = { llm: [ @@ -97,13 +100,22 @@ const CHECKS_BY_TYPE = { "users", "status", "metadata", + "feedback", "cost", "duration", "tokens", "radar", ], - trace: ["tags", "users", "status", "duration", "metadata", "radar"], - thread: ["tags", "users", "metadata", "radar"], + trace: [ + "tags", + "users", + "status", + "feedback", + "duration", + "metadata", + "radar", + ], + thread: ["tags", "users", "feedback", "metadata", "radar"], } const editCheck = (filters, id, params) => { diff --git a/packages/frontend/pages/radars/index.tsx b/packages/frontend/pages/radars/index.tsx index 4265a589..35992020 100644 --- a/packages/frontend/pages/radars/index.tsx +++ b/packages/frontend/pages/radars/index.tsx @@ -283,7 +283,7 @@ function RadarCard({ id, initialData }) { - {hasAccess(user.role, "radars", "edit") && ( + {hasAccess(user.role, "radars", "update") && ( diff --git a/packages/frontend/pages/reset-password.tsx b/packages/frontend/pages/reset-password.tsx index 358e3e9d..a42bde25 100644 --- a/packages/frontend/pages/reset-password.tsx +++ b/packages/frontend/pages/reset-password.tsx @@ -22,11 +22,6 @@ export default function UpdatePassword() { const [loading, setLoading] = useState(false) const router = useRouter() - const { token, email } = router.query as { - token: string - email: string - } - const form = useForm({ initialValues: { password: "", @@ -57,7 +52,7 @@ export default function UpdatePassword() { setJwt(token) - analytics.track("Join") + analytics.track("Password Reset") } catch (error) { console.error(error) setLoading(false) @@ -66,7 +61,7 @@ export default function UpdatePassword() { return ( - + diff --git a/packages/frontend/pages/team.tsx b/packages/frontend/pages/team.tsx index d08df832..4e0de332 100644 --- a/packages/frontend/pages/team.tsx +++ b/packages/frontend/pages/team.tsx @@ -165,7 +165,7 @@ function SAMLConfig() { Sign on URL: diff --git a/packages/frontend/pages/users/index.tsx b/packages/frontend/pages/users/index.tsx index c5b48916..4d5855cf 100644 --- a/packages/frontend/pages/users/index.tsx +++ b/packages/frontend/pages/users/index.tsx @@ -11,7 +11,10 @@ import { IconUsers } from "@tabler/icons-react" import { NextSeo } from "next-seo" import Router from "next/router" import analytics from "../../utils/analytics" -import { useAppUserList } from "@/utils/dataHooks" +import { useAppUserList, useProjectInfiniteSWR } from "@/utils/dataHooks" +import SearchBar from "@/components/blocks/SearchBar" +import { useState } from "react" +import { useDebouncedValue } from "@mantine/hooks" const columns = [ { @@ -35,11 +38,21 @@ const columns = [ ] export default function Users() { - const { users, isLoading, isValidating } = useAppUserList() + const [search, setSearch] = useState("") + const [debouncedSearch] = useDebouncedValue(search, 200) + + const { + data: users, + loading, + validating, + loadMore, + } = useProjectInfiniteSWR( + `/external-users${debouncedSearch ? `?search=${debouncedSearch}` : ""}`, + ) return ( - + diff --git a/packages/frontend/utils/datatable.tsx b/packages/frontend/utils/datatable.tsx index e6806efc..3b3611c3 100644 --- a/packages/frontend/utils/datatable.tsx +++ b/packages/frontend/utils/datatable.tsx @@ -202,7 +202,10 @@ export function feedbackColumn(withRelatedRuns = false) { : (props) => { const run = props.row.original - return + const feedback = run.feedback || run.parentFeedback + const isParentFeedback = !run.feedback && run.parentFeedback + + return } return columnHelper.accessor("feedback", { diff --git a/packages/shared/checks/index.ts b/packages/shared/checks/index.ts index c6f6129f..4a1157e9 100644 --- a/packages/shared/checks/index.ts +++ b/packages/shared/checks/index.ts @@ -28,6 +28,7 @@ export const CHECKS: Check[] = [ id: "type", width: 110, defaultValue: "llm", + searchable: true, options: [ { label: "LLM Call", @@ -138,6 +139,7 @@ export const CHECKS: Check[] = [ multiple: true, id: "types", options: () => `/filters/feedback`, + getItemValue: (item) => JSON.stringify(item), }, ], }, @@ -157,7 +159,23 @@ export const CHECKS: Check[] = [ width: 100, id: "users", options: () => `/filters/users`, - // render: (item) => // todo + searchable: true, + getItemValue: (item) => `${item.id}`, + customSearch: (search, item) => { + const searchTerm = search.toLowerCase().trim() + + const toCheck = [ + item.external_id, + item.props?.email, + item.props?.name, + item.props?.firstName, + item.props?.lastName, + ] + + return toCheck.some((check) => + check?.toLowerCase().includes(searchTerm), + ) + }, }, ], }, @@ -175,6 +193,7 @@ export const CHECKS: Check[] = [ type: "select", width: 100, id: "key", + searchable: true, options: () => `/filters/metadata`, }, { @@ -447,6 +466,7 @@ export const CHECKS: Check[] = [ placeholder: "Select radars", multiple: true, options: () => `/filters/radars`, + searchable: true, }, ], }, @@ -559,6 +579,7 @@ export const CHECKS: Check[] = [ width: 230, defaultValue: ["person", "location", "email", "cc", "phone", "ssn"], multiple: true, + searchable: true, options: [ { label: "Name", @@ -663,6 +684,7 @@ export const CHECKS: Check[] = [ id: "persona", defaultValue: "helpful", width: 140, + searchable: true, options: [ { label: "Helpful Assistant", @@ -762,6 +784,7 @@ export const CHECKS: Check[] = [ defaultValue: ["b", "c"], multiple: true, width: 200, + searchable: true, options: [ { label: "is a subset of", diff --git a/packages/shared/checks/types.ts b/packages/shared/checks/types.ts index 0d163ceb..f49f63b6 100644 --- a/packages/shared/checks/types.ts +++ b/packages/shared/checks/types.ts @@ -12,8 +12,10 @@ export type CheckParam = { step?: number width?: number placeholder?: string - render?: (value: any) => React.ReactNode defaultValue?: string | number | boolean | string[] + searchable?: boolean + getItemValue?: (item: any) => string // custom function to get value from item, for selects + customSearch?: (query: string, item: any) => boolean // custom search function for search in selects multiple?: boolean options?: | Array<{ label: string; value: string }>