diff --git a/apps/web/src/actions/connectedEvaluations/update.test.ts b/apps/web/src/actions/connectedEvaluations/update.test.ts new file mode 100644 index 000000000..670913d43 --- /dev/null +++ b/apps/web/src/actions/connectedEvaluations/update.test.ts @@ -0,0 +1,121 @@ +import { Providers } from '@latitude-data/core/browser' +import * as factories from '@latitude-data/core/factories' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { updateConnectedEvaluationAction } from './update' + +const mocks = vi.hoisted(() => { + return { + getSession: vi.fn(), + } +}) + +vi.mock('$/services/auth/getSession', () => ({ + getSession: mocks.getSession, +})) + +describe('updateConnectedEvaluationAction', () => { + let workspace: any + let project: any + let user: any + let connectedEvaluation: any + + beforeEach(async () => { + const prompt = factories.helpers.createPrompt({ + provider: 'Latitude', + model: 'gpt-4o', + }) + const setup = await factories.createProject({ + providers: [{ type: Providers.OpenAI, name: 'Latitude' }], + name: 'Default Project', + documents: { + foo: { + content: prompt, + }, + }, + }) + workspace = setup.workspace + project = setup.project + user = setup.user + + // Create a connected evaluation using a factory + const evaluation = await factories.createLlmAsJudgeEvaluation({ workspace }) + const { commit } = await factories.createDraft({ project, user }) + const { documentLog } = await factories.createDocumentLog({ + document: setup.documents[0]!, + commit, + }) + + connectedEvaluation = await factories.createConnectedEvaluation({ + workspace, + evaluationUuid: evaluation.uuid, + documentUuid: documentLog.documentUuid, + }) + }) + + describe('unauthorized', () => { + it('errors when the user is not authenticated', async () => { + mocks.getSession.mockReturnValue(null) + + const [_, error] = await updateConnectedEvaluationAction({ + id: connectedEvaluation.id, + data: { live: true }, + }) + + expect(error!.name).toEqual('UnauthorizedError') + }) + }) + + describe('authorized', () => { + beforeEach(() => { + mocks.getSession.mockReturnValue({ + user, + workspace: { id: workspace.id, name: workspace.name }, + }) + }) + + it('successfully updates a connected evaluation', async () => { + const [data, error] = await updateConnectedEvaluationAction({ + id: connectedEvaluation.id, + data: { live: true }, + }) + + expect(error).toBeNull() + expect(data).toBeDefined() + expect(data!.id).toEqual(connectedEvaluation.id) + expect(data!.live).toEqual(true) + }) + + it('returns an error when the connected evaluation is not found', async () => { + const [_, error] = await updateConnectedEvaluationAction({ + id: 9999, // Non-existent ID + data: { live: true }, + }) + + expect(error).toBeDefined() + expect(error!.name).toEqual('NotFoundError') + }) + + it('does not update fields that are not provided', async () => { + const [data, _] = await updateConnectedEvaluationAction({ + id: connectedEvaluation.id, + data: { live: connectedEvaluation.live }, // Provide the required 'live' field + }) + + expect(data).toBeDefined() + expect(data!.id).toEqual(connectedEvaluation.id) + expect(data!.live).toEqual(connectedEvaluation.live) // Should remain unchanged + }) + + it('handles invalid input data', async () => { + const [_, error] = await updateConnectedEvaluationAction({ + id: connectedEvaluation.id, + // @ts-expect-error - Testing invalid input + data: { live: 'not a boolean' }, + }) + + expect(error).toBeDefined() + expect(error!.name).toEqual('ZodError') + }) + }) +}) diff --git a/apps/web/src/actions/connectedEvaluations/update.ts b/apps/web/src/actions/connectedEvaluations/update.ts new file mode 100644 index 000000000..194ff9adf --- /dev/null +++ b/apps/web/src/actions/connectedEvaluations/update.ts @@ -0,0 +1,33 @@ +'use server' + +import { ConnectedEvaluationsRepository } from '@latitude-data/core/repositories' +import { updateConnectedEvaluation } from '@latitude-data/core/services/connectedEvaluations/update' +import { z } from 'zod' + +import { authProcedure } from '../procedures' + +export const updateConnectedEvaluationAction = authProcedure + .createServerAction() + .input( + z.object({ + id: z.number(), + data: z.object({ + live: z.boolean(), + }), + }), + ) + .handler(async ({ input, ctx }) => { + const connectedEvaluationsScope = new ConnectedEvaluationsRepository( + ctx.workspace.id, + ) + const connectedEvaluation = await connectedEvaluationsScope + .find(input.id) + .then((r) => r.unwrap()) + + const result = await updateConnectedEvaluation({ + connectedEvaluation, + data: input.data, + }) + + return result.unwrap() + }) diff --git a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/EvaluationStats.tsx b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/EvaluationStats.tsx index cbc3e8caa..0f27f3f89 100644 --- a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/EvaluationStats.tsx +++ b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/EvaluationStats.tsx @@ -7,7 +7,7 @@ import { EvaluationDto } from '@latitude-data/core/browser' import { ConnectedDocumentWithMetadata } from '@latitude-data/core/repositories' import { Skeleton, Text } from '@latitude-data/web-ui' import { formatCostInMillicents } from '$/app/_lib/formatUtils' -import useConnectedDocuments from '$/stores/connectedEvaluations' +import useConnectedDocuments from '$/stores/connectedDocuments' export function Stat({ label, value }: { label: string; value?: string }) { return ( diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/LiveEvaluationToggle/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/LiveEvaluationToggle/index.tsx new file mode 100644 index 000000000..dad21177b --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/LiveEvaluationToggle/index.tsx @@ -0,0 +1,58 @@ +import { useCallback } from 'react' + +import { EvaluationDto } from '@latitude-data/core/browser' +import { + Label, + SwitchToogle, + useCurrentCommit, + useCurrentProject, + useToast, +} from '@latitude-data/web-ui' +import useConnectedEvaluations from '$/stores/connectedEvaluations' + +export default function LiveEvaluationToggle({ + documentUuid, + evaluation, +}: { + documentUuid: string + evaluation: EvaluationDto +}) { + const { toast } = useToast() + const { commit } = useCurrentCommit() + const { project } = useCurrentProject() + const { data, update, isUpdating } = useConnectedEvaluations({ + documentUuid, + projectId: project.id, + commitUuid: commit.uuid, + }) + const connectedEvaluation = data.find( + (ev) => ev.evaluationId === evaluation.id, + ) + const toggleLive = useCallback(async () => { + if (!connectedEvaluation) return + + const live = !connectedEvaluation.live + const [_, error] = await update({ + id: connectedEvaluation.id, + data: { live }, + }) + if (error) return + + toast({ + title: 'Successfully updated evaluation', + description: live ? 'Evaluation is now live' : 'Evaluation is now paused', + }) + }, [connectedEvaluation, update]) + if (!connectedEvaluation) return null + + return ( +
+ + +
+ ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/index.tsx index 7723339d7..d69178761 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/index.tsx @@ -5,6 +5,8 @@ import { Button, Icon, TableWithHeader } from '@latitude-data/web-ui' import { ROUTES } from '$/services/routes' import Link from 'next/link' +import LiveEvaluationToggle from './LiveEvaluationToggle' + export function Actions({ evaluation, projectId, @@ -24,6 +26,10 @@ export function Actions({ return (
+