diff --git a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/index.tsx b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/index.tsx index 19a5a9931..ca019a551 100644 --- a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/index.tsx +++ b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { Suspense, useCallback, useMemo, useState } from 'react' +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { promptConfigSchema, @@ -43,11 +43,15 @@ export default function EvaluationEditor({ () => promptConfigSchema({ providers: providers ?? [] }), [providers], ) - const { metadata } = useMetadata({ - prompt: value, - withParameters: SERIALIZED_DOCUMENT_LOG_FIELDS, - configSchema, - }) + const { metadata, runReadMetadata } = useMetadata() + + useEffect(() => { + runReadMetadata({ + prompt: value, + withParameters: SERIALIZED_DOCUMENT_LOG_FIELDS, + configSchema, + }) + }, []) const save = useCallback( (val: string) => { @@ -62,8 +66,13 @@ export default function EvaluationEditor({ const onChange = useCallback( (value: string) => { setValue(value) + runReadMetadata({ + prompt: value, + withParameters: SERIALIZED_DOCUMENT_LOG_FIELDS, + configSchema, + }) }, - [setValue], + [setValue, runReadMetadata], ) if (!evaluation) return null diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/index.tsx index 30b0d28f4..ade55fc98 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/index.tsx @@ -5,6 +5,7 @@ import React, { createContext, Suspense, useCallback, + useEffect, useMemo, useState, } from 'react' @@ -159,12 +160,6 @@ export default function DocumentEditor({ { trailing: true }, ) - const onChange = useCallback((value: string) => { - setIsSaved(false) - setValue(value) - debouncedSave(value) - }, []) - const readDocumentContent = useCallback( async (path: string) => { return _documents.find((d) => d.path === path)?.content @@ -204,12 +199,31 @@ export default function DocumentEditor({ [providers], ) - const { metadata } = useMetadata({ - prompt: value, - fullPath: document.path, - referenceFn: readDocument, - configSchema, - }) + const { metadata, runReadMetadata } = useMetadata() + + useEffect(() => { + runReadMetadata({ + prompt: value, + fullPath: document.path, + referenceFn: readDocument, + configSchema, + }) + }, []) + + const onChange = useCallback( + (newValue: string) => { + setIsSaved(false) + setValue(newValue) + debouncedSave(newValue) + runReadMetadata({ + prompt: newValue, + fullPath: document.path, + referenceFn: readDocument, + configSchema, + }) + }, + [runReadMetadata, readDocument, configSchema, document.path], + ) const { execute: executeRequestSuggestionAction, diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/CreateBatchEvaluationModal/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/CreateBatchEvaluationModal/index.tsx index bb571ff41..99a3c813c 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/CreateBatchEvaluationModal/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/Actions/CreateBatchEvaluationModal/index.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import { DocumentVersion, EvaluationDto } from '@latitude-data/core/browser' import { Button, CloseTrigger, Modal } from '@latitude-data/web-ui' @@ -32,10 +32,14 @@ export default function CreateBatchEvaluationModal({ }, }) - const { metadata } = useMetadata({ - prompt: document.content ?? '', - fullPath: document.path, - }) + const { metadata, runReadMetadata } = useMetadata() + + useEffect(() => { + runReadMetadata({ + prompt: document.content ?? '', + fullPath: document.path, + }) + }, []) const form = useRunBatchForm({ documentMetadata: metadata }) const onRunBatch = useCallback(() => { diff --git a/apps/web/src/hooks/useMetadata.ts b/apps/web/src/hooks/useMetadata.ts index 612e152a3..5be707d4b 100644 --- a/apps/web/src/hooks/useMetadata.ts +++ b/apps/web/src/hooks/useMetadata.ts @@ -1,41 +1,26 @@ 'use client' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { ConversationMetadata, readMetadata } from '@latitude-data/compiler' import { useDebouncedCallback } from 'use-debounce' type Props = Parameters[0] -export function useMetadata(props: Props) { - const [propsQueue, setPropsQueue] = useState(props) - useEffect(() => { - setPropsQueue(props) - }, Object.values(props)) - - const [isLoading, setIsLoading] = useState(false) +export function useMetadata() { const [metadata, setMetadata] = useState() + const [isLoading, setIsLoading] = useState(false) const runReadMetadata = useDebouncedCallback( - (props: Props, onSuccess: (data: ConversationMetadata) => void) => { - readMetadata(props).then(onSuccess) + async (props: Props) => { + setIsLoading(true) + const metadata = await readMetadata(props) + setMetadata(metadata) + setIsLoading(false) }, 500, { trailing: true }, ) - useEffect(() => { - if (isLoading) return - if (!propsQueue) return - - setIsLoading(true) - setPropsQueue(null) - - runReadMetadata(propsQueue, (m) => { - setMetadata(m) - setIsLoading(false) - }) - }, [isLoading, propsQueue]) - - return { metadata, isLoading } + return { metadata, runReadMetadata, isLoading } } diff --git a/packages/core/src/repositories/providerLogsRepository.test.ts b/packages/core/src/repositories/providerLogsRepository.test.ts new file mode 100644 index 000000000..0d9969f86 --- /dev/null +++ b/packages/core/src/repositories/providerLogsRepository.test.ts @@ -0,0 +1,256 @@ +import { randomUUID } from 'crypto' + +import { beforeEach, describe, expect, it } from 'vitest' + +import { + Commit, + DocumentVersion, + LogSources, + ProviderApiKey, + Providers, + Workspace, +} from '../browser' +import { NotFoundError } from '../lib' +import * as factories from '../tests/factories' +import { ProviderLogsRepository } from './providerLogsRepository' + +describe('ProviderLogsRepository', () => { + let workspace: Workspace + let document: DocumentVersion + let commit: Commit + let provider: ProviderApiKey + let providerLogsRepository: ProviderLogsRepository + + beforeEach(async () => { + const { + workspace: createdWorkspace, + commit: createdCommit, + documents, + providers, + } = await factories.createProject({ + providers: [{ type: Providers.OpenAI, name: 'openai' }], + documents: { + doc1: factories.helpers.createPrompt({ + provider: 'openai', + content: 'content', + }), + }, + }) + workspace = createdWorkspace + document = documents[0]! + commit = createdCommit + provider = providers[0]! + providerLogsRepository = new ProviderLogsRepository(workspace.id) + }) + + describe('findByUuid', () => { + it('returns the provider log when found', async () => { + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + }) + + const providerLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + }) + + const result = await providerLogsRepository.findByUuid(providerLog.uuid) + + expect(result.ok).toBe(true) + expect(result.unwrap()).toEqual( + expect.objectContaining({ + uuid: providerLog.uuid, + workspaceId: workspace.id, + }), + ) + }) + + it('returns a NotFoundError when the provider log is not found', async () => { + const result = await providerLogsRepository.findByUuid(randomUUID()) + + expect(result.ok).toBe(false) + expect(() => result.unwrap()).toThrowError(NotFoundError) + }) + }) + + describe('findByDocumentUuid', () => { + it('returns provider logs for a document', async () => { + const documentUuid = document.documentUuid + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + }) + + const providerLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + }) + + const result = + await providerLogsRepository.findByDocumentUuid(documentUuid) + + expect(result.ok).toBe(true) + expect(result.unwrap()).toContainEqual( + expect.objectContaining({ + uuid: providerLog.uuid, + documentLogUuid: documentLog.uuid, + }), + ) + }) + + it('respects limit and offset options', async () => { + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + }) + + await Promise.all( + Array.from({ length: 3 }).map(() => + factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + }), + ), + ) + + const result = await providerLogsRepository.findByDocumentUuid( + document.documentUuid, + { + limit: 2, + offset: 1, + }, + ) + + expect(result.ok).toBe(true) + expect(result.unwrap()).toHaveLength(2) + }) + }) + + describe('findLastByDocumentLogUuid', () => { + it('returns the latest provider log for a document log', async () => { + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + skipProviderLogs: true, + }) + + const firstLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + generatedAt: new Date('2024-01-01'), + }) + + const lastLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + generatedAt: new Date('2024-01-02'), + }) + + const result = await providerLogsRepository.findLastByDocumentLogUuid( + documentLog.uuid, + ) + + expect(result.ok).toBe(true) + expect(result.unwrap().uuid).toBe(lastLog.uuid) + expect(result.unwrap().uuid).not.toBe(firstLog.uuid) + }) + + it('returns a NotFoundError when the document log is not found', async () => { + const result = + await providerLogsRepository.findLastByDocumentLogUuid(randomUUID()) + + expect(result.ok).toBe(false) + expect(() => result.unwrap()).toThrowError(NotFoundError) + }) + + it('returns an error when documentLogUuid is undefined', async () => { + const result = + await providerLogsRepository.findLastByDocumentLogUuid(undefined) + + expect(result.ok).toBe(false) + expect(() => result.unwrap()).toThrowError(NotFoundError) + }) + }) + + describe('findByDocumentLogUuid', () => { + it('returns all provider logs for a document log', async () => { + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + skipProviderLogs: true, + }) + + const firstLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + generatedAt: new Date('2024-01-01'), + }) + + const secondLog = await factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + generatedAt: new Date('2024-01-02'), + }) + + const result = await providerLogsRepository.findByDocumentLogUuid( + documentLog.uuid, + ) + + expect(result.ok).toBe(true) + const logs = result.unwrap() + expect(logs).toHaveLength(2) + expect(logs.map((log) => log.uuid)).toEqual( + expect.arrayContaining([firstLog.uuid, secondLog.uuid]), + ) + }) + + it('respects limit and offset options', async () => { + const { documentLog } = await factories.createDocumentLog({ + document, + commit, + }) + + await Promise.all( + Array.from({ length: 3 }).map(() => + factories.createProviderLog({ + workspace, + documentLogUuid: documentLog.uuid, + providerId: provider.id, + providerType: provider.provider, + source: LogSources.Playground, + }), + ), + ) + + const result = await providerLogsRepository.findByDocumentLogUuid( + documentLog.uuid, + { limit: 2, offset: 1 }, + ) + + expect(result.ok).toBe(true) + expect(result.unwrap()).toHaveLength(2) + }) + }) +}) diff --git a/packages/core/src/repositories/providerLogsRepository.ts b/packages/core/src/repositories/providerLogsRepository.ts index 2e79eac1e..38ce1ce35 100644 --- a/packages/core/src/repositories/providerLogsRepository.ts +++ b/packages/core/src/repositories/providerLogsRepository.ts @@ -29,6 +29,10 @@ export class ProviderLogsRepository extends Repository { async findByDocumentUuid(documentUuid: string, opts: QueryOptions = {}) { const query = this.scope + .innerJoin( + documentLogs, + eq(providerLogs.documentLogUuid, documentLogs.uuid), + ) .where(eq(documentLogs.documentUuid, documentUuid)) .orderBy(asc(providerLogs.generatedAt))