diff --git a/.gitignore b/.gitignore index f09a9a35a..86637c003 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,8 @@ docker/pgdata/ # Next.js next-env.d.ts -apps/web/public/uploads +// File uploads in development +tmp # Misc TODO.md diff --git a/apps/database/fly.toml b/apps/database/fly.toml deleted file mode 100644 index 0afbceb33..000000000 --- a/apps/database/fly.toml +++ /dev/null @@ -1,68 +0,0 @@ -# fly.toml app configuration file generated for latitude-llm-database on 2024-08-13T08:54:27+02:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'latitude-llm-database' -primary_region = 'mad' - -[env] - PRIMARY_REGION = 'mad' - -[[mounts]] - source = 'pg_data' - destination = '/data' - -[[services]] - protocol = 'tcp' - internal_port = 5432 - auto_start_machines = true - - [[services.ports]] - port = 5432 - handlers = ['pg_tls'] - - [services.concurrency] - type = 'connections' - hard_limit = 1000 - soft_limit = 1000 - -[[services]] - protocol = 'tcp' - internal_port = 5433 - auto_start_machines = true - - [[services.ports]] - port = 5433 - handlers = ['pg_tls'] - - [services.concurrency] - type = 'connections' - hard_limit = 1000 - soft_limit = 1000 - -[checks] - [checks.pg] - port = 5500 - type = 'http' - interval = '15s' - timeout = '10s' - path = '/flycheck/pg' - - [checks.role] - port = 5500 - type = 'http' - interval = '15s' - timeout = '10s' - path = '/flycheck/role' - - [checks.vm] - port = 5500 - type = 'http' - interval = '15s' - timeout = '10s' - path = '/flycheck/vm' - -[[metrics]] - port = 9187 - path = '/metrics' diff --git a/apps/web/src/actions/datasets/preview.ts b/apps/web/src/actions/datasets/preview.ts index 38cb7fed0..79cfefe1a 100644 --- a/apps/web/src/actions/datasets/preview.ts +++ b/apps/web/src/actions/datasets/preview.ts @@ -16,5 +16,7 @@ export const previewDatasetAction = authProcedure .handler(async ({ ctx, input }) => { const repo = new DatasetsRepository(ctx.workspace.id) const dataset = await repo.find(input.id).then((r) => r.unwrap()) - return await previewDataset({ dataset }).then((r) => r.unwrap()) + return await previewDataset({ dataset, prependIndex: true }).then((r) => + r.unwrap(), + ) }) diff --git a/apps/web/src/actions/evaluations/runBatch.ts b/apps/web/src/actions/evaluations/runBatch.ts index 5db530095..ed031fb31 100644 --- a/apps/web/src/actions/evaluations/runBatch.ts +++ b/apps/web/src/actions/evaluations/runBatch.ts @@ -1,59 +1,107 @@ 'use server' +import { readMetadata } from '@latitude-data/compiler' import { DatasetsRepository, - DocumentVersionsRepository, EvaluationsRepository, } from '@latitude-data/core/repositories' import { setupJobs } from '@latitude-data/jobs' import { nanoid } from 'nanoid' import { z } from 'zod' +import { createServerActionProcedure } from 'zsa' -import { authProcedure } from '../procedures' +import { widthDocument } from '../procedures' -export const runBatchEvaluationAction = authProcedure +const USER_DECIDED_TO_IGNORE_THIS_PARAMETER = -1 + +const withDataset = createServerActionProcedure(widthDocument) + .input(z.object({ datasetId: z.number() })) + .handler(async ({ input, ctx }) => { + const datasetsRepo = new DatasetsRepository(ctx.workspace.id) + const dataset = await datasetsRepo + .find(input.datasetId) + .then((r) => r.unwrap()) + + return { ...ctx, dataset } + }) + +function isValidParameter(valueIndex: number | undefined, headers: string[]) { + if (valueIndex === undefined) return false + if (valueIndex === USER_DECIDED_TO_IGNORE_THIS_PARAMETER) return true + const hasIndex = headers[valueIndex] + return hasIndex !== undefined +} +function parameterErrorMessage({ + param, + message, +}: { + param: string + message: string +}) { + return `${param}: ${message}` +} + +export const runBatchEvaluationAction = withDataset .createServerAction() - .input( - z.object({ - datasetId: z.number(), - projectId: z.number(), - documentUuid: z.string(), - commitUuid: z.string(), + .input(async ({ ctx }) => { + return z.object({ + evaluationIds: z.array(z.number()), fromLine: z.number().optional(), toLine: z.number().optional(), - parameters: z.record(z.number()).optional(), - evaluationIds: z.array(z.number()), - }), - ) + parameters: z + .record(z.number()) + .optional() + .superRefine(async (parameters = {}, refineCtx) => { + const metadata = await readMetadata({ + prompt: ctx.document.content ?? '', + fullPath: ctx.document.path, + }) + const docParams = metadata.parameters + const headers = ctx.dataset.fileMetadata.headers + const paramKeys = Object.keys(parameters) + Array.from(docParams).forEach((key) => { + const existsInDocument = paramKeys.includes(key) + + if (!existsInDocument) { + refineCtx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['parameters', key], + message: parameterErrorMessage({ + param: key, + message: 'Is not a valid parameter in this document', + }), + }) + } + + const valueIndex = isValidParameter(parameters[key], headers) + + if (!valueIndex) { + refineCtx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['parameters', key], + message: parameterErrorMessage({ + param: key, + message: + 'Has not a valid header assigned in this dataset. If you want to keep empty this parameter choose "Leave empty in that parameter"', + }), + }) + } + }) + }), + }) + }) .handler(async ({ input, ctx }) => { const evaluationsRepo = new EvaluationsRepository(ctx.workspace.id) const evaluations = await evaluationsRepo .filterById(input.evaluationIds) .then((r) => r.unwrap()) - - const datasetsRepo = new DatasetsRepository(ctx.workspace.id) - const dataset = await datasetsRepo - .find(input.datasetId) - .then((r) => r.unwrap()) - - const docsRepo = new DocumentVersionsRepository(ctx.workspace.id) - const document = await docsRepo - .getDocumentAtCommit({ - projectId: input.projectId, - commitUuid: input.commitUuid, - documentUuid: input.documentUuid, - }) - .then((r) => r.unwrap()) - const queues = setupJobs() - evaluations.forEach((evaluation) => { const batchId = `evaluation:${evaluation.id}:${nanoid(5)}` - queues.defaultQueue.jobs.enqueueRunBatchEvaluationJob({ evaluation, - dataset, - document, + dataset: ctx.dataset, + document: ctx.document, fromLine: input.fromLine, toLine: input.toLine, parametersMap: input.parameters, diff --git a/apps/web/src/actions/procedures/index.ts b/apps/web/src/actions/procedures/index.ts index 966d57a3f..f9611cbc3 100644 --- a/apps/web/src/actions/procedures/index.ts +++ b/apps/web/src/actions/procedures/index.ts @@ -1,5 +1,8 @@ import { UnauthorizedError } from '@latitude-data/core/lib/errors' -import { ProjectsRepository } from '@latitude-data/core/repositories' +import { + DocumentVersionsRepository, + ProjectsRepository, +} from '@latitude-data/core/repositories' import { getCurrentUser } from '$/services/auth/getCurrentUser' import { z } from 'zod' import { createServerActionProcedure } from 'zsa' @@ -29,3 +32,18 @@ export const withProject = createServerActionProcedure(authProcedure) return { ...ctx, project } }) + +export const widthDocument = createServerActionProcedure(withProject) + .input(z.object({ commitUuid: z.string(), documentUuid: z.string() })) + .handler(async ({ input, ctx }) => { + const repo = new DocumentVersionsRepository(ctx.workspace.id) + const document = await repo + .getDocumentAtCommit({ + projectId: ctx.project.id, + commitUuid: input.commitUuid, + documentUuid: input.documentUuid, + }) + .then((r) => r.unwrap()) + + return { ...ctx, document } + }) diff --git a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/ConnectedDocumentsTable/index.tsx b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/ConnectedDocumentsTable/index.tsx index 54957b189..959b18dc7 100644 --- a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/ConnectedDocumentsTable/index.tsx +++ b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/dashboard/_components/ConnectedDocumentsTable/index.tsx @@ -136,7 +136,7 @@ export default function ConnectedDocumentsTable({ .detail({ id: document.projectId }) .commits.detail({ uuid: HEAD_COMMIT }) .documents.detail({ uuid: document.documentUuid }) - .evaluations.detail(document.evaluationUuid).root, + .evaluations.detail(document.evaluationId).root, ) } /> diff --git a/apps/web/src/app/(private)/evaluations/_components/ActiveEvaluations/Table/index.tsx b/apps/web/src/app/(private)/evaluations/_components/ActiveEvaluations/Table/index.tsx index bbd5217db..18daa62b0 100644 --- a/apps/web/src/app/(private)/evaluations/_components/ActiveEvaluations/Table/index.tsx +++ b/apps/web/src/app/(private)/evaluations/_components/ActiveEvaluations/Table/index.tsx @@ -15,7 +15,7 @@ import { useNavigate } from '$/hooks/useNavigate' import { ROUTES } from '$/services/routes' import Link from 'next/link' -export const ActiveEvaluationsTableRow = ({ +const ActiveEvaluationsTableRow = ({ evaluation, onSelect, }: { 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 new file mode 100644 index 000000000..9d69e3aae --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/index.tsx @@ -0,0 +1,30 @@ +'use client' + +import { EvaluationDto } from '@latitude-data/core/browser' +import { TableWithHeader } from '@latitude-data/web-ui' +import { ROUTES } from '$/services/routes' +import Link from 'next/link' + +export function Actions({ + evaluation, + projectId, + commitUuid, + documentUuid, +}: { + evaluation: EvaluationDto + projectId: string + commitUuid: string + documentUuid: string +}) { + const href = ROUTES.projects + .detail({ id: Number(projectId) }) + .commits.detail({ uuid: commitUuid }) + .documents.detail({ uuid: documentUuid }) + .evaluations.detail(evaluation.id).createBatch + + return ( + + Run batch evaluation + + ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/DatasetForm/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/DatasetForm/index.tsx new file mode 100644 index 000000000..5554132b7 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/DatasetForm/index.tsx @@ -0,0 +1,163 @@ +import { useMemo, useState } from 'react' + +import { Dataset } from '@latitude-data/core/browser' +import { + FormFieldGroup, + Input, + NumeredList, + Select, + SelectOption, + SwitchInput, +} from '@latitude-data/web-ui' + +function LineRangeInputs({ + disabled, + fromDefaultValue, + toDefaultValue, + max, +}: { + disabled: boolean + fromDefaultValue: number | undefined + toDefaultValue: number | undefined + max: number | undefined +}) { + const [to, setTo] = useState(toDefaultValue) + return ( + + + setTo(Number(e.target.value))} + min={0} + max={max} + /> + + ) +} + +export default function DatasetForm({ + onParametersChange, + selectedDataset, + headers, + wantAllLines, + fromLine, + toLine, + datasets, + isLoadingDatasets, + parametersList, + onToggleAllLines, + onSelectDataset, + errors, +}: { + onParametersChange: (param: string) => (header: string) => void + parametersList: string[] + wantAllLines: boolean + fromLine: number | undefined + toLine: number | undefined + headers: SelectOption[] + selectedDataset: Dataset | null + datasets: Dataset[] + isLoadingDatasets: boolean + onSelectDataset: (value: string) => void + onToggleAllLines: (checked: boolean) => void + errors: Record | undefined +}) { + const paramaterErrors = useMemo(() => { + if (!errors) return {} + if (!errors.parameters) return {} + + const paramErrors = errors.parameters + if (!Array.isArray(paramErrors)) return {} + + return paramErrors.reduce( + (acc, error) => { + const parts = error.split(': ') + const param = parts[0] + const message = parts[1] + if (!param || !message) return acc + + const prevMessage = acc[param] || [] + prevMessage.push(message) + acc[param] = prevMessage + return acc + }, + {} as Record, + ) + }, [errors]) + const datasetOptions = useMemo( + () => datasets.map((ds) => ({ value: ds.id, label: ds.name })), + [datasets], + ) + return ( + <> + + + + ))} + + ) : null} + + + + ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/index.tsx new file mode 100644 index 000000000..adc2528f8 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/index.tsx @@ -0,0 +1,108 @@ +'use client' + +import { useCallback } from 'react' + +import { ConversationMetadata } from '@latitude-data/compiler' +import { DocumentVersion, EvaluationDto } from '@latitude-data/core/browser' +import { Button, CloseTrigger, Modal, useToast } from '@latitude-data/web-ui' +import { useNavigate } from '$/hooks/useNavigate' +import { ROUTES } from '$/services/routes' + +import DatasetForm from './DatasetForm' +import { useRunBatch } from './useRunBatch' +import { useRunBatchForm } from './useRunBatchForm' + +export default function CreateBatchEvaluationModal({ + document, + evaluation, + documentMetadata, + projectId, + commitUuid, +}: { + projectId: string + commitUuid: string + document: DocumentVersion + evaluation: EvaluationDto + documentMetadata: ConversationMetadata +}) { + const { toast } = useToast() + const navigate = useNavigate() + const documentUuid = document.documentUuid + const goToDetail = useCallback(() => { + navigate.push( + ROUTES.projects + .detail({ id: Number(projectId) }) + .commits.detail({ uuid: commitUuid }) + .documents.detail({ uuid: documentUuid }) + .evaluations.detail(evaluation.id).root, + ) + }, [evaluation.id, projectId, commitUuid, documentUuid]) + const { runBatch, errors, isRunningBatch } = useRunBatch({ + document, + projectId, + commitUuid, + onSuccess: () => { + toast({ + title: 'Success', + description: 'Batch evaluation is processing', + }) + goToDetail() + }, + }) + const form = useRunBatchForm({ documentMetadata }) + const onRunBatch = useCallback(() => { + runBatch({ + datasetId: form.selectedDataset?.id, + evaluationIds: [evaluation.id], + fromLine: form.fromLine, + toLine: form.toLine, + wantAllLines: form.wantAllLines, + parameters: form.parameters, + }) + }, [ + evaluation.id, + runBatch, + form.fromLine, + form.toLine, + form.selectedDataset, + form.parameters, + form.wantAllLines, + ]) + + return ( + + + + + } + > + + + ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatch.ts b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatch.ts new file mode 100644 index 000000000..ffdc1b311 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatch.ts @@ -0,0 +1,61 @@ +import { useCallback } from 'react' + +import { DocumentVersion } from '@latitude-data/core/browser' +import { runBatchEvaluationAction } from '$/actions/evaluations/runBatch' +import useLatitudeAction from '$/hooks/useLatitudeAction' + +export type RunBatchParameters = Record +export function useRunBatch({ + document, + projectId, + commitUuid, + onSuccess, +}: { + projectId: string + document: DocumentVersion + commitUuid: string + onSuccess: () => void +}) { + const { + error, + execute: run, + isPending: isRunning, + } = useLatitudeAction(runBatchEvaluationAction, { onSuccess }) + const errors = error?.fieldErrors + const runBatch = useCallback( + async ({ + evaluationIds, + wantAllLines, + datasetId, + parameters, + fromLine, + toLine, + }: { + datasetId: number | undefined + fromLine: number | undefined + toLine: number | undefined + wantAllLines: boolean + evaluationIds: number[] + parameters: RunBatchParameters + }) => { + await run({ + projectId: Number(projectId), + documentUuid: document.documentUuid, + commitUuid, + evaluationIds, + datasetId: datasetId!, + fromLine: wantAllLines ? undefined : fromLine, + toLine: wantAllLines ? undefined : toLine, + // @ts-ignore + parameters, + }) + }, + [run, projectId, document.documentUuid, commitUuid], + ) + + return { + runBatch, + isRunningBatch: isRunning, + errors, + } +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatchForm.ts b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatchForm.ts new file mode 100644 index 000000000..e1d2c203d --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/_components/CreateBatchEvaluationModal/useRunBatchForm.ts @@ -0,0 +1,81 @@ +import { useCallback, useMemo, useState } from 'react' + +import { ConversationMetadata } from '@latitude-data/compiler' +import { Dataset } from '@latitude-data/core/browser' +import { SelectOption } from '@latitude-data/web-ui' +import useDatasets from '$/stores/datasets' + +import { RunBatchParameters } from './useRunBatch' + +function buildEmptyParameters(parameters: string[]) { + return parameters.reduce((acc, key) => { + acc[key] = undefined + return acc + }, {} as RunBatchParameters) +} + +export function useRunBatchForm({ + documentMetadata, +}: { + documentMetadata: ConversationMetadata +}) { + const parametersList = useMemo( + () => Array.from(documentMetadata.parameters), + [documentMetadata.parameters], + ) + const { data: datasets, isLoading: isLoadingDatasets } = useDatasets() + const [selectedDataset, setSelectedDataset] = useState(null) + const [headers, setHeaders] = useState([]) + const [wantAllLines, setAllRows] = useState(true) + const [fromLine, setFromLine] = useState(undefined) + const [toLine, setToLine] = useState(undefined) + const [parameters, setParameters] = useState(() => + buildEmptyParameters(parametersList), + ) + + const onParameterChange = useCallback( + (param: string) => (header: string) => { + setParameters((prev) => ({ + ...prev, + [param]: selectedDataset?.fileMetadata?.headers?.indexOf?.(header), + })) + }, + [selectedDataset], + ) + + const onSelectDataset = useCallback( + async (value: string) => { + const ds = datasets.find((ds) => ds.id === Number(value)) + if (!ds) return + + setSelectedDataset(ds) + setParameters(buildEmptyParameters(parametersList)) + setFromLine(0) + setToLine(ds.fileMetadata.rowCount) + setHeaders([ + { value: '-1', label: '-- Leave this parameter empty' }, + ...ds.fileMetadata.headers.map((header) => ({ + value: header, + label: header, + })), + ]) + }, + [parametersList, datasets], + ) + return { + datasets, + isLoadingDatasets, + selectedDataset, + headers, + wantAllLines, + fromLine, + toLine, + parameters, + parametersList, + onParameterChange, + onSelectDataset, + setAllRows, + setFromLine, + setToLine, + } +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/page.tsx new file mode 100644 index 000000000..782f0c901 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/create-batch/page.tsx @@ -0,0 +1,44 @@ +import { readMetadata } from '@latitude-data/compiler' +import { EvaluationsRepository } from '@latitude-data/core/repositories' +import { getDocumentByUuidCached } from '$/app/(private)/_data-access' +import { getCurrentUser } from '$/services/auth/getCurrentUser' + +import CreateBatchEvaluationModal from './_components/CreateBatchEvaluationModal' + +export default async function ConnectionEvaluationModal({ + params, +}: { + params: { + projectId: string + commitUuid: string + documentUuid: string + evaluationId: string + } +}) { + const { workspace } = await getCurrentUser() + const evaluationScope = new EvaluationsRepository(workspace.id) + const evaluation = await evaluationScope + .find(params.evaluationId) + .then((r) => r.unwrap()) + const projectId = Number(params.projectId) + const documentUuid = params.documentUuid + const commitUuid = params.commitUuid + const document = await getDocumentByUuidCached({ + projectId, + commitUuid, + documentUuid, + }) + const metadata = await readMetadata({ + prompt: document.content ?? '', + fullPath: document.path, + }) + return ( + + ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/layout.tsx new file mode 100644 index 000000000..76fabd48f --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/layout.tsx @@ -0,0 +1,67 @@ +import { ReactNode } from 'react' + +import { EvaluationsRepository } from '@latitude-data/core/repositories' +import { TableWithHeader, Text } from '@latitude-data/web-ui' +import BreadcrumpLink from '$/components/BreadcrumpLink' +import { Breadcrump } from '$/components/layouts/AppLayout/Header' +import { getCurrentUser } from '$/services/auth/getCurrentUser' +import { ROUTES } from '$/services/routes' + +import { Actions } from './_components/Actions' + +export default async function ConnectedEvaluationLayout({ + params, + children, +}: { + children: ReactNode + params: { + projectId: string + commitUuid: string + documentUuid: string + evaluationId: string + } +}) { + const { workspace } = await getCurrentUser() + const evaluationScope = new EvaluationsRepository(workspace.id) + const evaluation = await evaluationScope + .find(params.evaluationId) + .then((r) => r.unwrap()) + return ( +
+ {children} + + ), + }, + { name: {evaluation.name} }, + ]} + /> + } + actions={ + + } + table={<>} + /> +
+ ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/page.tsx new file mode 100644 index 000000000..603e00efb --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/page.tsx @@ -0,0 +1,3 @@ +export default function ConnectedEvaluationsPage() { + return null // --> layout.tsx +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationUuid]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationUuid]/page.tsx deleted file mode 100644 index 0786632dd..000000000 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationUuid]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { getEvaluationByUuidCached } from '$/app/(private)/_data-access' - -export default async function EvaluationPage({ - params: { evaluationUuid }, -}: { - params: { evaluationUuid: string } -}) { - const evaluation = await getEvaluationByUuidCached(evaluationUuid) - return ( -
-

{evaluation.name}

-

{evaluation.description || 'No description for this evaluation'}

-
- ) -} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/_components/BatchEvaluationsTable.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/_components/BatchEvaluationsTable.tsx index c2fa3330f..f5e8aedd8 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/_components/BatchEvaluationsTable.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/_components/BatchEvaluationsTable.tsx @@ -45,7 +45,7 @@ export default function BatchEvaluationsTable({ .detail({ id: project.id }) .commits.detail({ uuid: commit.uuid }) .documents.detail({ uuid: document.documentUuid }) - .evaluations.detail(evaluation.uuid).root, + .evaluations.detail(evaluation.id).root, ) } > @@ -57,13 +57,11 @@ export default function BatchEvaluationsTable({ e.stopPropagation()}> diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationEditor.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/Editor/index.tsx similarity index 94% rename from apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationEditor.tsx rename to apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/Editor/index.tsx index 86898d9fe..cda28e02e 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationEditor.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/Editor/index.tsx @@ -2,15 +2,6 @@ import React, { ReactNode, useEffect, useState } from 'react' import { Text } from '@latitude-data/web-ui' -interface EvaluationEditorProps { - items: { - uuid: string - name: string - type: 'evaluation' | 'template' - data: any - }[] -} - const className = 'flex-1 min-h-[450px] min-w-0 rounded-lg border bg-muted flex flex-col' @@ -53,12 +44,21 @@ const TabsContent: React.FC<{ children: ReactNode }> = ({ children }) => (
{children}
) -export default function EvaluationEditor({ items }: EvaluationEditorProps) { +export default function EvaluationEditor({ + items, +}: { + items: { + uuid: string + name: string + type: 'evaluation' | 'template' + data: any + }[] +}) { const [activeTab, setActiveTab] = useState(items[0]?.uuid ?? '') useEffect(() => { if (items.length > 0) { - const activeTabExists = items.some((item) => item.uuid === activeTab) + const activeTabExists = items.some((item) => item?.uuid === activeTab) if (!activeTabExists) { setActiveTab(items[items.length - 1]!.uuid) } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/List/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/List/index.tsx new file mode 100644 index 000000000..e7b4d2530 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/List/index.tsx @@ -0,0 +1,54 @@ +import React from 'react' + +import { Input, SelectableCard, Text } from '@latitude-data/web-ui' + +interface EvaluationListProps { + items: { + uuid: string + name: string + description: string + type: 'evaluation' | 'template' + }[] + selectedItem: string | undefined + onSelectItem: (uuid: string) => void + searchTerm: string + onSearchChange: (term: string) => void +} + +export default function EvaluationList({ + items, + selectedItem, + onSelectItem, + searchTerm, + onSearchChange, +}: EvaluationListProps) { + return ( +
+ onSearchChange(e.target.value)} + className='w-full p-2 mb-4 border rounded-lg' + /> +
    + {items.map((item) => ( + onSelectItem(item.uuid)} + /> + ))} +
+ {items.length === 0 && ( +
+ + No evaluations or templates found + +
+ )} +
+ ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/index.tsx new file mode 100644 index 000000000..578290f68 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectEvaluationModal/index.tsx @@ -0,0 +1,177 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { xorBy } from 'lodash-es' + +import { + EvaluationDto, + EvaluationTemplateCategory, +} from '@latitude-data/core/browser' +import { + Button, + CloseTrigger, + Icon, + Modal, + TableWithHeader, + Text, +} from '@latitude-data/web-ui' +import { connectEvaluationsAction } from '$/actions/evaluations/connect' +import useLatitudeAction from '$/hooks/useLatitudeAction' +import { useNavigate } from '$/hooks/useNavigate' +import { ROUTES } from '$/services/routes' +import useEvaluations from '$/stores/evaluations' +import useEvaluationTemplates from '$/stores/evaluationTemplates' +import Link from 'next/link' + +import EvaluationEditor from './Editor' +import EvaluationList from './List' + +type SelectableItem = { + uuid: string + name: string + description: string + type: 'evaluation' | 'template' + data: EvaluationDto | EvaluationTemplateCategory +} + +export default function ConnectionEvaluationModal({ + projectId, + commitUuid, + documentUuid, +}: { + projectId: string + commitUuid: string + documentUuid: string +}) { + const [selectedItem, setSelectedItem] = useState() + const { execute, isPending: isConnecting } = useLatitudeAction( + connectEvaluationsAction, + ) + const navigate = useNavigate() + const { data: usedEvaluations, mutate } = useEvaluations({ + params: { documentUuid }, + }) + const { data: evaluations, isLoading: isLoadingEvaluations } = + useEvaluations() + const { data: templates, isLoading: isLoadingTemplates } = + useEvaluationTemplates() + const [searchTerm, setSearchTerm] = useState('') + + const selectableItems: SelectableItem[] = useMemo(() => { + const evaluationItems = xorBy( + evaluations, + usedEvaluations, + (ev) => ev.id, + ).map((e) => ({ + uuid: e.uuid, + name: e.name, + description: e.description, + type: 'evaluation' as const, + data: e, + })) + const templateItems = templates.map((t) => ({ + uuid: t.id.toString(), + name: t.name, + description: t.description, + type: 'template' as const, + data: t, + })) + return [...evaluationItems, ...templateItems] + }, [evaluations, usedEvaluations, templates]) + + const filteredItems = useMemo(() => { + return selectableItems.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()), + ) + }, [selectableItems, searchTerm]) + + const handleSelectItem = (uuid: string) => { + setSelectedItem(selectableItems.find((item) => item.uuid === uuid)?.uuid) + } + + const createConnection = useCallback(async () => { + const templateId = templates?.find( + (template) => template.id.toString() === selectedItem, + )?.id + const evaluationUuid = evaluations.find( + (evaluation) => evaluation.uuid === selectedItem, + )?.uuid + + const [data] = await execute({ + projectId, + templateIds: templateId ? [templateId] : [], + evaluationUuids: evaluationUuid ? [evaluationUuid] : [], + documentUuid, + }) + + if (data) { + mutate() + const connectedEvaluation = data[0]! + navigate.push( + ROUTES.projects + .detail({ id: Number(projectId) }) + .commits.detail({ uuid: commitUuid }) + .documents.detail({ uuid: documentUuid }) + .evaluations.detail(connectedEvaluation.evaluationId).root, + ) + } + }, [execute, projectId, documentUuid, selectedItem, templates, evaluations]) + + const isLoadingList = isLoadingEvaluations || isLoadingTemplates + const item = selectableItems.find((item) => item.uuid === selectedItem) + return ( + { + navigate.push( + ROUTES.projects + .detail({ id: Number(projectId) }) + .commits.detail({ uuid: commitUuid }) + .documents.detail({ uuid: documentUuid }).evaluations.root, + ) + }} + footer={ + <> + + + + } + > +
+ Evaluations and Templates} + actions={ + + + + } + table={ +
+ {!isLoadingList ? ( + + ) : null} + +
+ } + /> +
+
+ ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationList.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationList.tsx deleted file mode 100644 index 7803c1a9a..000000000 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/EvaluationList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react' - -import { Icon, Input, Text } from '@latitude-data/web-ui' - -interface EvaluationListProps { - items: { - uuid: string - name: string - description: string - type: 'evaluation' | 'template' - }[] - selectedItems: string[] - onSelectItem: (uuid: string) => void - searchTerm: string - onSearchChange: (term: string) => void -} - -export default function EvaluationList({ - items, - selectedItems, - onSelectItem, - searchTerm, - onSearchChange, -}: EvaluationListProps) { - return ( -
- onSearchChange(e.target.value)} - className='w-full p-2 mb-4 border rounded-lg' - /> -
    - {items.map((item) => ( -
  • onSelectItem(item.uuid)} - > - {selectedItems.includes(item.uuid) && ( - - )} -
    - - {item.name} - - - {item.description.length > 45 - ? `${item.description.slice(0, 42)}...` - : item.description} - - - {item.type === 'evaluation' ? 'Evaluation' : 'Template'} - -
    -
  • - ))} -
- {items.length === 0 && ( -
- - No evaluations or templates found - -
- )} -
- ) -} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/index.tsx deleted file mode 100644 index cc4f43e01..000000000 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/ConnectToEvaluationStep/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useMemo, useState } from 'react' -import { xorBy } from 'lodash-es' - -import { - EvaluationDto, - EvaluationTemplateWithCategory, -} from '@latitude-data/core/browser' -import { Button, Icon, TableWithHeader, Text } from '@latitude-data/web-ui' -import { ROUTES } from '$/services/routes' -import useEvaluations from '$/stores/evaluations' -import useEvaluationTemplates from '$/stores/evaluationTemplates' -import Link from 'next/link' -import { useParams } from 'next/navigation' - -import EvaluationEditor from './EvaluationEditor' -import EvaluationList from './EvaluationList' - -type SelectableItem = { - uuid: string - name: string - description: string - type: 'evaluation' | 'template' - data: EvaluationDto | EvaluationTemplateWithCategory -} - -export default function ConnectEvaluationStep({ - selectedItems, - setSelectedItems, -}: { - selectedItems: string[] - setSelectedItems: (items: string[]) => void -}) { - const { documentUuid } = useParams() - const { data: usedEvaluations } = useEvaluations({ - params: { documentUuid: documentUuid as string }, - }) - const { data: evaluations, isLoading: isLoadingEvaluations } = - useEvaluations() - const { data: templates, isLoading: isLoadingTemplates } = - useEvaluationTemplates() - const [searchTerm, setSearchTerm] = useState('') - - const selectableItems: SelectableItem[] = useMemo(() => { - const evaluationItems = xorBy( - evaluations, - usedEvaluations, - (ev) => ev.id, - ).map((e) => ({ - uuid: e.uuid, - name: e.name, - description: e.description, - type: 'evaluation' as const, - data: e, - })) - const templateItems = templates.map((t) => ({ - uuid: t.id.toString(), - name: t.name, - description: t.description, - type: 'template' as const, - data: t, - })) - return [...evaluationItems, ...templateItems] - }, [evaluations, usedEvaluations, templates]) - - const filteredItems = useMemo(() => { - return selectableItems.filter((item) => - item.name.toLowerCase().includes(searchTerm.toLowerCase()), - ) - }, [selectableItems, searchTerm]) - - const handleSelectItem = (uuid: string) => { - setSelectedItems( - selectedItems.includes(uuid) - ? selectedItems.filter((id) => id !== uuid) - : [...selectedItems, uuid], - ) - } - - if (isLoadingEvaluations || isLoadingTemplates) return null - - return ( -
- Evaluations and Templates} - actions={ - - - - } - table={ -
- - - selectedItems.includes(item.uuid), - )} - /> -
- } - /> -
- ) -} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/MapPromptDataToDatasetStep/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/MapPromptDataToDatasetStep/index.tsx deleted file mode 100644 index cda6a54f3..000000000 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/_components/MapPromptDataToDatasetStep/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function MapPromptDataToDatasetStep() { - return null -} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/page.tsx index 2d182141b..f2d1d967d 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/page.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/dashboard/connect/page.tsx @@ -1,39 +1,7 @@ -'use client' +import ConnectEvaluationModal from './_components/ConnectEvaluationModal' -import { useState } from 'react' - -import { Button, CloseTrigger, Modal, Steps } from '@latitude-data/web-ui' -import { connectEvaluationsAction } from '$/actions/evaluations/connect' -import useLatitudeAction from '$/hooks/useLatitudeAction' -import { useNavigate } from '$/hooks/useNavigate' -import { ROUTES } from '$/services/routes' -import useEvaluations from '$/stores/evaluations' -import useEvaluationTemplates from '$/stores/evaluationTemplates' - -import ConnectEvaluationStep from './_components/ConnectToEvaluationStep' -import MapPromptDataToDatasetStep from './_components/MapPromptDataToDatasetStep' - -function title(step: number) { - switch (step) { - case 1: - return 'Which evaluations do you want to connect?' - case 2: - return 'What data do you want to analyze?' - } -} - -function description(step: number) { - switch (step) { - case 1: - return 'Batch evaluations allow you to analyze a specific amount of logs generated from a dataset.' - // TODO: Probably description in step 2 is different from step 1 - case 2: - return 'Batch evaluations allow you to analyze a specific amount of logs generated from a dataset.' - } -} - -export default function ConnectionEvaluationModal({ - params: { projectId, commitUuid, documentUuid }, +export default async function ConnectionEvaluationModalPage({ + params, }: { params: { projectId: string @@ -41,84 +9,13 @@ export default function ConnectionEvaluationModal({ documentUuid: string } }) { - const [selectedItems, setSelectedItems] = useState([]) - const [step, setStep] = useState(1) - const { execute, isPending: isCreatingConnections } = useLatitudeAction( - connectEvaluationsAction, - ) - const navigate = useNavigate() - const { data: templates } = useEvaluationTemplates() - const { data: evaluations, mutate } = useEvaluations() - const createConnections = async () => { - const templateIds = selectedItems - .filter((id) => - templates?.some((template) => template.id.toString() === id), - ) - .map(Number) - const evaluationUuids = selectedItems.filter((id) => - evaluations?.some((evaluation) => evaluation.uuid === id), - ) - - const [data] = await execute({ - projectId, - templateIds, - evaluationUuids, - documentUuid, - }) - - if (data) { - mutate() - setStep(2) - } - } - + const documentUuid = params.documentUuid + const commitUuid = params.commitUuid return ( - { - navigate.push( - ROUTES.projects - .detail({ id: Number(projectId) }) - .commits.detail({ uuid: commitUuid }) - .documents.detail({ uuid: documentUuid }).evaluations.root, - ) - }} - steps={{ total: 2, current: step }} - footer={ - <> - - {step === 1 && ( - - )} - {step === 2 && ( - - )} - - } - > - - - - - + ) } diff --git a/apps/web/src/components/BreadcrumpLink/index.tsx b/apps/web/src/components/BreadcrumpLink/index.tsx index 8b2dea4d7..a7c8ccf1e 100644 --- a/apps/web/src/components/BreadcrumpLink/index.tsx +++ b/apps/web/src/components/BreadcrumpLink/index.tsx @@ -1,15 +1,18 @@ -import { Text } from '@latitude-data/web-ui' +import { Icon, Text } from '@latitude-data/web-ui' import Link from 'next/link' export default function BreadcrumpLink({ name, href, + showBackIcon, }: { name: string href: string + showBackIcon?: boolean }) { return ( - + + {showBackIcon && } {name} ) diff --git a/apps/web/src/components/layouts/AppLayout/Header/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/index.tsx index cb5edb4fe..2c7ea5298 100644 --- a/apps/web/src/components/layouts/AppLayout/Header/index.tsx +++ b/apps/web/src/components/layouts/AppLayout/Header/index.tsx @@ -38,17 +38,25 @@ type IBreadCrumb = { name: string | ReactNode } -function Breadcrump({ breadcrumbs }: { breadcrumbs: IBreadCrumb[] }) { +export function Breadcrump({ + breadcrumbs, + showLogo = false, +}: { + breadcrumbs: IBreadCrumb[] + showLogo?: boolean +}) { return (
    -
  • - -
  • - {breadcrumbs.length === 0 ? null : ( -
  • - -
  • - )} + {showLogo ? ( + <> +
  • + +
  • +
  • + +
  • + + ) : null} {breadcrumbs.map((breadcrumb, idx) => { const isLast = idx === breadcrumbs.length - 1 return ( @@ -108,7 +116,7 @@ export default function AppHeader({ return (
    - +