Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
AppLocalStorage,
useLocalStorage,
} from '@latitude-data/web-ui/hooks/useLocalStorage'
import { IssuesOverviewCard } from './IssuesOverviewCard'

function sumCounts(
counts: Record<LogSources, number> | undefined,
Expand Down Expand Up @@ -284,6 +285,7 @@ export function RunsPage({
/>
) : (
<div className='w-full h-full flex flex-col gap-6 p-6 overflow-hidden relative'>
<IssuesOverviewCard />
<div className='w-full h-full flex items-center justify-center gap-2 py-9 px-6 border border-border border-dashed rounded-xl'>
<Text.H5 color='foregroundMuted'>No run selected</Text.H5>
</div>
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/app/api/projects/[projectId]/issues/overview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { z } from 'zod'
import { authHandler } from '$/middlewares/authHandler'
import { errorHandler } from '$/middlewares/errorHandler'
import { ProjectsRepository } from '@latitude-data/core/repositories'
import { Workspace } from '@latitude-data/core/schema/models/types/Workspace'
import { NextRequest, NextResponse } from 'next/server'
import { annotatedResultStatsByProject } from '@latitude-data/core/data-access/issues/annotatedResultStatsByProject'

const paramsSchema = z.object({
projectId: z.coerce.number(),
})

export type IssuesOverviewResponse = Awaited<
ReturnType<typeof annotatedResultStatsByProject>
>

export const GET = errorHandler(
authHandler(
async (
_request: NextRequest,
{
params,
workspace,
}: {
params: {
projectId: string
}
workspace: Workspace
},
) => {
const { projectId } = paramsSchema.parse({
projectId: params.projectId,
})
const projectsRepo = new ProjectsRepository(workspace.id)
const project = await projectsRepo.find(projectId).then((r) => r.unwrap())
const stats = await annotatedResultStatsByProject({ project })

return NextResponse.json(stats, { status: 200 })
},
),
)
57 changes: 57 additions & 0 deletions apps/web/src/components/IssuesOverviewCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import { Text } from '@latitude-data/web-ui/atoms/Text'
import { Icon } from '@latitude-data/web-ui/atoms/Icons'
import { useCurrentProject } from '$/app/providers/ProjectProvider'
import { useIssuesOverview } from './issuesOverviewStore'

export function IssuesOverviewCard() {
const { project } = useCurrentProject()
const { data, isLoading } = useIssuesOverview({ projectId: project.id })
// TODO: Implement
const progress = isLoading ? 0 : 0

return (
<div className='w-full p-6 border border-border rounded-xl bg-background'>
<div className='flex items-start justify-between mb-6'>
<div>
<Text.H4>Issues overview is now available!</Text.H4>
<Text.H6 color='foregroundMuted'>
Keep annotating to discover more issues.
</Text.H6>
</div>
</div>

<div className='flex items-center gap-4'>
<div className='flex-1 relative'>
{/* Background bar */}
<div className='h-2 w-full bg-muted rounded-full overflow-hidden'>
{/* Progress bar */}
<div
className='h-full bg-primary transition-all duration-300'
style={{ width: `${Math.min(progress, 100)}%` }}
/>
</div>
</div>

{/* Lightbulb icon */}
<div className='flex-shrink-0 w-10 h-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center relative'>
<div className='absolute -top-1 left-1/2 -translate-x-1/2 text-yellow-500'>
<Icon name='sparkles' className='w-3 h-3' />
</div>
<Icon name='lightBulb' className='text-yellow-500 w-5 h-5' />
</div>

<div className='flex-1 h-2 bg-muted rounded-full' />
</div>

<div className='flex items-center justify-between mt-3'>
<div className='flex items-center gap-2'>
<Text.H6 color='foregroundMuted'>Insight discovery ·</Text.H6>
<Text.H6>{data.issuesWithAnnotations}</Text.H6>
</div>
<Text.H6 color='foregroundMuted'>{data.totalAnnotations}</Text.H6>
</div>
</div>
)
}
36 changes: 36 additions & 0 deletions apps/web/src/components/IssuesOverviewCard/issuesOverviewStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IssuesOverviewResponse } from '$/app/api/projects/[projectId]/issues/overview/route'
import useFetcher from '$/hooks/useFetcher'
import { ROUTES } from '$/services/routes'
import { useMemo } from 'react'
import useSWR, { SWRConfiguration } from 'swr'

const EMPTY_OVERVIEW: IssuesOverviewResponse = {
issuesCount: 0,
annotatedCount: 0,
}
export function useIssuesOverview(
{
projectId,
}: {
projectId: number
},
swrConfig?: SWRConfiguration<IssuesOverviewResponse, any>,
) {
const route = ROUTES.api.projects.detail(projectId).issues.overview.root
const fetcher = useFetcher<IssuesOverviewResponse, IssuesOverviewResponse>(
route,
)
const { data = EMPTY_OVERVIEW, isLoading } = useSWR<IssuesOverviewResponse>(
['issues-overview', projectId],
fetcher,
swrConfig,
)

return useMemo(
() => ({
data,
isLoading,
}),
[data, isLoading],
)
}
5 changes: 5 additions & 0 deletions apps/web/src/services/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ export const API_ROUTES = {
stats: {
root: `${projectRoot}/stats`,
},
issues: {
overview: {
root: `${projectRoot}/issues/overview`,
},
},
commits: {
root: `${projectRoot}/commits`,
detail: (commitUuid: string) => ({
Expand Down
4 changes: 4 additions & 0 deletions packages/constants/src/issues/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ export const ISSUE_JOBS_MAX_ATTEMPTS = 3
export const ISSUE_JOBS_GENERATE_DETAILS_THROTTLE = 6 * 60 * 60 * 1000 // 6 hours
export const ISSUE_JOBS_MERGE_COMMON_THROTTLE = 24 * 60 * 60 * 1000 // 1 day
export const ISSUE_JOBS_DISCOVER_RESULT_DELAY = 60 * 1000 // 1 minute

// This is the minimum number of manually annotated evaluation results required
// for automatic issue creation to be enabled for a project.
export const MINIMUM_ANNOTATED_THRESHOLD = 10
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
EvaluationResultsV2Repository,
IssuesRepository,
} from '../../repositories'
import { Project } from '../../schema/models/types/Project'

export async function annotatedResultStatsByProject({
project,
}: {
project: Project
}) {
const issuesRepo = new IssuesRepository(project.workspaceId)
const evalResultRepo = new EvaluationResultsV2Repository(project.workspaceId)
const issuesCount = await issuesRepo.countByProject({ project })
const annotatedCount =
await evalResultRepo.failedManuallyAnnotatedCountByProject({ project })

return {
issuesCount,
annotatedCount,
}
}
26 changes: 22 additions & 4 deletions packages/core/src/repositories/evaluationResultsV2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import serializeProviderLog from '../services/providerLogs/serialize'
import { CommitsRepository } from './commitsRepository'
import { EvaluationsV2Repository } from './evaluationsV2Repository'
import Repository from './repositoryV2'
import { Project } from '../schema/models/types/Project'

const tt = getTableColumns(evaluationResultsV2)

Expand Down Expand Up @@ -345,14 +346,14 @@ export class EvaluationResultsV2Repository extends Repository<EvaluationResultV2
totalTokens:
evaluation.type === EvaluationType.Llm
? sql`sum((${evaluationResultsV2.metadata}->>'tokens')::bigint)`
.mapWith(Number)
.as('total_tokens')
.mapWith(Number)
.as('total_tokens')
: sql`0`.mapWith(Number),
totalCost:
evaluation.type === EvaluationType.Llm
? sql`sum((${evaluationResultsV2.metadata}->>'cost')::bigint)`
.mapWith(Number)
.as('total_cost')
.mapWith(Number)
.as('total_cost')
: sql`0`.mapWith(Number),
}

Expand Down Expand Up @@ -584,6 +585,23 @@ export class EvaluationResultsV2Repository extends Repository<EvaluationResultV2
return Result.ok<number>(result.count)
}

async failedManuallyAnnotatedCountByProject({ project }: { project: Project }) {
const result = await this.db
.select({ count: count() })
.from(evaluationResultsV2)
.innerJoin(commits, eq(commits.id, evaluationResultsV2.commitId))
.where(
and(
this.scopeFilter,
eq(commits.projectId, project.id),
eq(evaluationResultsV2.hasPassed, false),
sql`${evaluationResultsV2.metadata}->>'reason' IS NOT NULL AND ${evaluationResultsV2.metadata}->>'reason' != ''`,
),
)

return result[0]?.count ?? 0
}

async selectForDocumentSuggestion({
commitId,
evaluationUuid,
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/repositories/issuesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ export class IssuesRepository extends Repository<Issue> {
return this.db.select(tt).from(issues).where(this.scopeFilter).$dynamic()
}

async countByProject({ project }: { project: Project }) {
const result = await this.db
.select({
count: sql<number>`COUNT(*)::integer`,
})
.from(issues)
.where(and(this.scopeFilter, eq(issues.projectId, project.id)))

return result[0]?.count ?? 0
}

async lock({ id, wait }: { id: number; wait?: boolean }) {
// .for('no key update', { noWait: true }) is bugged in drizzle!
// https://github.com/drizzle-team/drizzle-orm/issues/3554
Expand Down Expand Up @@ -224,8 +235,8 @@ export class IssuesRepository extends Repository<Issue> {
'isResolved',
),
isRegressed: sql<boolean>`(
${issues.resolvedAt} IS NOT NULL
AND ${issues.ignoredAt} IS NULL
${issues.resolvedAt} IS NOT NULL
AND ${issues.ignoredAt} IS NULL
AND ${subquery.lastSeenDate} > ${issues.resolvedAt}
)`.as('isRegressed'),
isEscalating: sql<boolean>`(
Expand Down