diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/MetadataInfoTabs/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/MetadataInfoTabs/index.tsx index 6c14900d0..efe23b463 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/MetadataInfoTabs/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/MetadataInfoTabs/index.tsx @@ -1,41 +1,48 @@ -import { ReactNode, useState } from 'react' +import { forwardRef, ReactNode, useState } from 'react' import { cn, TabSelector, TabSelectorOption } from '@latitude-data/web-ui' type RenderProps = { selectedTab: string } -export function MetadataInfoTabs({ - className, - tabs = [ - { label: 'Metadata', value: 'metadata' }, - { label: 'Messages', value: 'messages' }, - ], - children, -}: { +type Props = { children: (args: RenderProps) => ReactNode tabs?: TabSelectorOption[] className?: string -}) { - const [selectedTab, setSelectedTab] = useState('metadata') - return ( -
-
- -
-
-
- {children({ selectedTab })} +} +export const MetadataInfoTabs = forwardRef( + function MetadataInfoTabs( + { + className, + tabs = [ + { label: 'Metadata', value: 'metadata' }, + { label: 'Messages', value: 'messages' }, + ], + children, + }, + ref, + ) { + const [selectedTab, setSelectedTab] = useState('metadata') + return ( +
+
+ +
+
+
+ {children({ selectedTab })} +
-
- ) -} + ) + }, +) diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx index 3f85f9ec3..8e615d23d 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { RefObject, useCallback, useEffect, useRef, useState } from 'react' import { EvaluationDto, ProviderLogDto } from '@latitude-data/core/browser' import { @@ -7,8 +7,10 @@ import { } from '@latitude-data/core/repositories' import { Button, Icon, Modal, ReactStateDispatch } from '@latitude-data/web-ui' import useFetcher from '$/hooks/useFetcher' +import { useStickyNested } from '$/hooks/useStickyNested' import { ROUTES } from '$/services/routes' import useProviderLogs from '$/stores/providerLogs' +import { usePanelDomRef } from 'node_modules/@latitude-data/web-ui/src/ds/atoms/SplitPane' import useSWR from 'swr' import { MetadataInfoTabs } from '../../../../../_components/MetadataInfoTabs' @@ -67,17 +69,37 @@ export function EvaluationResultInfo({ evaluation, evaluationResult, providerLog, + tableRef, + sidebarWrapperRef, }: { evaluation: EvaluationDto evaluationResult: EvaluationResultWithMetadataAndErrors providerLog?: ProviderLogDto + tableRef: RefObject + sidebarWrapperRef: RefObject }) { const [selected, setSelected] = useState(null) const onClickOpen = useCallback(async () => { setSelected(evaluationResult.documentLogId) }, [evaluationResult.documentLogId]) + const ref = useRef(null) + const [target, setTarget] = useState(null) + useEffect(() => { + if (!ref.current) return + + setTarget(ref.current) + }, [ref.current]) + const scrollableArea = usePanelDomRef({ selfRef: target }) + const beacon = tableRef.current + useStickyNested({ + scrollableArea, + beacon, + target, + targetContainer: sidebarWrapperRef.current, + offset: 24, + }) return ( - <> +
{({ selectedTab }) => ( <> @@ -106,6 +128,6 @@ export function EvaluationResultInfo({ onOpenChange={setSelected} /> ) : null} - +
) } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultsTable.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultsTable.tsx index e0300ea15..e1bd72742 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultsTable.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultsTable.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react' import { capitalize } from 'lodash-es' import { @@ -66,152 +67,158 @@ export const ResultCellContent = ({ export type EvaluationResultRow = EvaluationResultWithMetadataAndErrors & { realtimeAdded?: boolean } -export const EvaluationResultsTable = ({ - evaluation, - evaluationResults, - selectedResult, - setSelectedResult, -}: { +type Props = { evaluation: EvaluationDto evaluationResults: EvaluationResultRow[] selectedResult: EvaluationResultRow | undefined setSelectedResult: ( log: EvaluationResultWithMetadataAndErrors | undefined, ) => void -}) => { - const searchParams = useSearchParams() - const page = searchParams.get('page') ?? '1' - const pageSize = searchParams.get('pageSize') ?? '25' - const document = useCurrentDocument() - const { commit } = useCurrentCommit() - const { project } = useCurrentProject() - const { data: pagination, isLoading } = useEvaluationResultsPagination({ - evaluationId: evaluation.id, - documentUuid: document.documentUuid, - commitUuid: commit.uuid, - projectId: project.id, - page, - pageSize, - }) +} +export const EvaluationResultsTable = forwardRef( + function EvaluationResultsTable( + { evaluation, evaluationResults, selectedResult, setSelectedResult }, + ref, + ) { + const searchParams = useSearchParams() + const page = searchParams.get('page') ?? '1' + const pageSize = searchParams.get('pageSize') ?? '25' + const document = useCurrentDocument() + const { commit } = useCurrentCommit() + const { project } = useCurrentProject() + const { data: pagination, isLoading } = useEvaluationResultsPagination({ + evaluationId: evaluation.id, + documentUuid: document.documentUuid, + commitUuid: commit.uuid, + projectId: project.id, + page, + pageSize, + }) - return ( - - } - > - - - Time - Version - Origin - Result - Cost - Tokens - - - - {evaluationResults.map((evaluationResult) => { - const error = getRunErrorFromErrorable(evaluationResult.error) - const cellColor = error ? 'destructiveMutedForeground' : 'foreground' - return ( - - setSelectedResult( - selectedResult?.id === evaluationResult.id - ? undefined - : evaluationResult, - ) - } - className={cn( - 'cursor-pointer border-b-[0.5px] h-12 max-h-12 border-border', - { - 'bg-secondary': selectedResult?.id === evaluationResult.id, - 'animate-flash': evaluationResult.realtimeAdded, - }, - )} - > - - - - - - -
- - - {evaluationResult.commit.version - ? `v${evaluationResult.commit.version}` - : 'Draft'} - - - - {evaluationResult.commit.title} - -
-
- - - {evaluationResult.source - ? capitalize(evaluationResult.source) - : '-'} - - - - {evaluationResult.result !== null ? ( - - ) : ( - - + return ( +
+ } + > + + + Time + Version + Origin + Result + Cost + Tokens + + + + {evaluationResults.map((evaluationResult) => { + const error = getRunErrorFromErrorable(evaluationResult.error) + const cellColor = error + ? 'destructiveMutedForeground' + : 'foreground' + return ( + + setSelectedResult( + selectedResult?.id === evaluationResult.id + ? undefined + : evaluationResult, + ) + } + className={cn( + 'cursor-pointer border-b-[0.5px] h-12 max-h-12 border-border', + { + 'bg-secondary': selectedResult?.id === evaluationResult.id, + 'animate-flash': evaluationResult.realtimeAdded, + }, )} - - - - {typeof evaluationResult.costInMillicents === 'number' - ? formatCostInMillicents(evaluationResult.costInMillicents) - : '-'} - - - - - {evaluationResult.tokens ?? '-'} - - - - ) - })} - -
- ) -} + > + + + + + + +
+ + + {evaluationResult.commit.version + ? `v${evaluationResult.commit.version}` + : 'Draft'} + + + + {evaluationResult.commit.title} + +
+
+ + + {evaluationResult.source + ? capitalize(evaluationResult.source) + : '-'} + + + + {evaluationResult.result !== null ? ( + + ) : ( + - + )} + + + + {typeof evaluationResult.costInMillicents === 'number' + ? formatCostInMillicents( + evaluationResult.costInMillicents, + ) + : '-'} + + + + + {evaluationResult.tokens ?? '-'} + + + + ) + })} + + + ) + }, +) diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/index.tsx index dd5ef4cd2..eca7e5eb4 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { EvaluationDto } from '@latitude-data/core/browser' import { type EvaluationResultWithMetadataAndErrors } from '@latitude-data/core/repositories' @@ -81,6 +81,8 @@ export function EvaluationResults({ evaluation: EvaluationDto evaluationResults: EvaluationResultWithMetadataAndErrors[] }) { + const tabelRef = useRef(null) + const sidebarWrapperRef = useRef(null) const { project } = useCurrentProject() const { commit } = useCurrentCommit() const document = useCurrentDocument() @@ -105,18 +107,16 @@ export function EvaluationResults({ fallbackData: serverData, }, ) - useEvaluationResultsSocket(evaluation, document, mutate) - return ( -
+
Evaluation Results -
-
+
+
{evaluationResults.length === 0 && ( 0 && ( )}
- {selectedResult && ( -
+ {selectedResult ? ( +
- )} + ) : null}
+
diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx index fa368bb9a..1392ab0e3 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx @@ -1,30 +1,39 @@ import { useMemo } from 'react' -import { AssistantMessage, Message, MessageRole } from '@latitude-data/compiler' +import { Message, MessageRole } from '@latitude-data/compiler' import { ProviderLogDto } from '@latitude-data/core/browser' import { MessageList, Text } from '@latitude-data/web-ui' -export function DocumentLogMessages({ +export function useGetProviderLogMessages({ providerLogs, }: { providerLogs?: ProviderLogDto[] }) { const providerLog = providerLogs?.[providerLogs.length - 1] + return useMemo(() => { + if (!providerLog) return { messages: [] as Message[], lastResponse: null } + if (!providerLog.response) { + return { messages: providerLog.messages, lastResponse: null } + } - const messages = useMemo(() => { - if (!providerLog) return [] as Message[] - if (!providerLog.response) return providerLog.messages - - const responseMessage = { + const lastResponse = { role: MessageRole.assistant, - content: providerLog.response, - toolCalls: providerLog.toolCalls, - } as AssistantMessage + content: providerLog!.response, + toolCalls: providerLog!.toolCalls, + } - return [...(providerLog.messages as Message[]), responseMessage] + return { + lastResponse, + messages: [ + ...(providerLog!.messages as Message[]), + lastResponse as unknown as Message, + ], + } }, [providerLog]) +} - if (!providerLog) { +export function DocumentLogMessages({ messages }: { messages: Message[] }) { + if (!messages.length) { return ( There are no messages generated for this log diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx index f063c81f0..ab3a0e50c 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react' +import { MessageRole, ToolCall } from '@latitude-data/compiler' import { ProviderLogDto } from '@latitude-data/core/browser' import { DocumentLogWithMetadataAndError } from '@latitude-data/core/repositories' -import { ClickToCopy, Text } from '@latitude-data/web-ui' +import { ClickToCopy, Message, Text } from '@latitude-data/web-ui' import { formatCostInMillicents, formatDuration } from '$/app/_lib/formatUtils' import { RunErrorMessage } from '$/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/RunErrorMessage' import useProviderApiKeys from '$/stores/providerApiKeys' @@ -154,8 +155,14 @@ function ProviderLogsMetadata({ export function DocumentLogMetadata({ documentLog, providerLogs = [], + lastResponse, }: { documentLog: DocumentLogWithMetadataAndError + lastResponse: { + role: MessageRole + content: string + toolCalls: ToolCall[] + } | null providerLogs?: ProviderLogDto[] }) { const providerLog = providerLogs[providerLogs.length - 1] @@ -187,6 +194,12 @@ export function DocumentLogMetadata({ documentLog={documentLog} /> ) : null} + {lastResponse ? ( +
+ Last Response + +
+ ) : null} ) } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx index 6deb82423..e03079a00 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx @@ -1,12 +1,16 @@ 'use client' +import { RefObject, useEffect, useRef, useState } from 'react' + import { ProviderLogDto } from '@latitude-data/core/browser' import { DocumentLogWithMetadataAndError } from '@latitude-data/core/repositories' import { Alert } from '@latitude-data/web-ui' +import { useStickyNested } from '$/hooks/useStickyNested' +import { usePanelDomRef } from 'node_modules/@latitude-data/web-ui/src/ds/atoms/SplitPane' import { MetadataInfoTabs } from '../../../../_components/MetadataInfoTabs' import { MetadataItem } from '../../../../../[documentUuid]/_components/MetadataItem' -import { DocumentLogMessages } from './Messages' +import { DocumentLogMessages, useGetProviderLogMessages } from './Messages' import { DocumentLogMetadata } from './Metadata' function DocumentLogMetadataLoading() { @@ -28,15 +32,36 @@ export function DocumentLogInfo({ isLoading = false, error, className, + tableRef, + sidebarWrapperRef, }: { documentLog: DocumentLogWithMetadataAndError providerLogs?: ProviderLogDto[] isLoading?: boolean error?: Error className?: string + tableRef?: RefObject + sidebarWrapperRef?: RefObject }) { + const ref = useRef(null) + const [target, setTarget] = useState(null) + useEffect(() => { + if (!ref.current) return + + setTarget(ref.current) + }, [ref.current]) + const scrollableArea = usePanelDomRef({ selfRef: target }) + const beacon = tableRef?.current + useStickyNested({ + scrollableArea, + beacon, + target, + targetContainer: sidebarWrapperRef?.current, + offset: 24, + }) + const { lastResponse, messages } = useGetProviderLogMessages({ providerLogs }) return ( - + {({ selectedTab }) => isLoading ? ( @@ -48,10 +73,11 @@ export function DocumentLogInfo({ )} {selectedTab === 'messages' && ( - + )} ) : ( diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogsTable.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogsTable.tsx index 584c0b996..2c8065366 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogsTable.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogsTable.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react' import { capitalize } from 'lodash-es' import { buildPagination } from '@latitude-data/core/lib/pagination/buildPagination' @@ -35,140 +36,145 @@ type DocumentLogRow = DocumentLogWithMetadataAndError & { realtimeAdded?: boolean } -export const DocumentLogsTable = ({ - documentLogs, - selectedLog, - setSelectedLog, -}: { +type Props = { documentLogs: DocumentLogRow[] selectedLog: DocumentLogWithMetadataAndError | undefined setSelectedLog: (log: DocumentLogWithMetadataAndError | undefined) => void -}) => { - const searchParams = useSearchParams() - const page = searchParams.get('page') ?? '1' - const pageSize = searchParams.get('pageSize') ?? '25' - const { commit } = useCurrentCommit() - const { project } = useCurrentProject() - const document = useCurrentDocument() - const { data: pagination, isLoading } = useDocumentLogsPagination({ - projectId: project.id, - commitUuid: commit.uuid, - documentUuid: document.documentUuid, - page, - pageSize, - }) - return ( - - } - > - - - Time - Version - Origin - Custom Identifier - Duration - Tokens - Cost - - - - {documentLogs.map((documentLog) => { - const error = getRunErrorFromErrorable(documentLog.error) - const cellColor = error ? 'destructiveMutedForeground' : 'foreground' - return ( - - setSelectedLog( - selectedLog?.uuid === documentLog.uuid - ? undefined - : documentLog, - ) - } - className={cn( - 'cursor-pointer border-b-[0.5px] h-12 max-h-12 border-border', - { - 'bg-secondary': selectedLog?.uuid === documentLog.uuid, - 'animate-flash': documentLog.realtimeAdded, - }, - )} - > - - - {relativeTime(documentLog.createdAt)} - - - -
- - - {documentLog.commit.version - ? `v${documentLog.commit.version}` - : 'Draft'} - - - - {documentLog.commit.title} - -
-
- - - {capitalize(documentLog.source || '')} - - - - - {documentLog.customIdentifier} - - - - - {formatDuration(documentLog.duration)} - - - - - {typeof documentLog.tokens === 'number' - ? documentLog.tokens - : '-'} - - - - - {typeof documentLog.costInMillicents === 'number' - ? formatCostInMillicents(documentLog.costInMillicents) - : '-'} - - -
- ) - })} -
-
- ) } +export const DocumentLogsTable = forwardRef( + function DocomentLogsTable( + { documentLogs, selectedLog, setSelectedLog }, + ref, + ) { + const searchParams = useSearchParams() + const page = searchParams.get('page') ?? '1' + const pageSize = searchParams.get('pageSize') ?? '25' + const { commit } = useCurrentCommit() + const { project } = useCurrentProject() + const document = useCurrentDocument() + const { data: pagination, isLoading } = useDocumentLogsPagination({ + projectId: project.id, + commitUuid: commit.uuid, + documentUuid: document.documentUuid, + page, + pageSize, + }) + return ( + + } + > + + + Time + Version + Origin + Custom Identifier + Duration + Tokens + Cost + + + + {documentLogs.map((documentLog) => { + const error = getRunErrorFromErrorable(documentLog.error) + const cellColor = error + ? 'destructiveMutedForeground' + : 'foreground' + return ( + + setSelectedLog( + selectedLog?.uuid === documentLog.uuid + ? undefined + : documentLog, + ) + } + className={cn( + 'cursor-pointer border-b-[0.5px] h-12 max-h-12 border-border', + { + 'bg-secondary': selectedLog?.uuid === documentLog.uuid, + 'animate-flash': documentLog.realtimeAdded, + }, + )} + > + + + {relativeTime(documentLog.createdAt)} + + + +
+ + + {documentLog.commit.version + ? `v${documentLog.commit.version}` + : 'Draft'} + + + + {documentLog.commit.title} + +
+
+ + + {capitalize(documentLog.source || '')} + + + + + {documentLog.customIdentifier} + + + + + {formatDuration(documentLog.duration)} + + + + + {typeof documentLog.tokens === 'number' + ? documentLog.tokens + : '-'} + + + + + {typeof documentLog.costInMillicents === 'number' + ? formatCostInMillicents(documentLog.costInMillicents) + : '-'} + + +
+ ) + })} +
+
+ ) + }, +) diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/index.tsx index aad0197bd..23bf0e4c8 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { DocumentLogWithMetadataAndError } from '@latitude-data/core/repositories' import { @@ -71,6 +71,8 @@ export function DocumentLogs({ }: { documentLogs: DocumentLogWithMetadataAndError[] }) { + const tableRef = useRef(null) + const sidebarWrapperRef = useRef(null) const searchParams = useSearchParams() const page = searchParams.get('page') const pageSize = searchParams.get('pageSize') @@ -106,20 +108,25 @@ export function DocumentLogs({ } return ( -
+
- +
+ +
{selectedLog && ( -
+
)} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/page.tsx index 3477e88ac..ec5f766e1 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/page.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/page.tsx @@ -27,7 +27,7 @@ export default async function DocumentPage({ pageSize: searchParams.pageSize as string | undefined, }) return ( -
+
} diff --git a/apps/web/src/hooks/useStickyNested.ts b/apps/web/src/hooks/useStickyNested.ts new file mode 100644 index 000000000..6b4d9ffce --- /dev/null +++ b/apps/web/src/hooks/useStickyNested.ts @@ -0,0 +1,83 @@ +import { useEffect } from 'react' + +function useUpdateWidthOnTargetContainerChange({ + target, + targetContainer, +}: { + target: HTMLElement | null + targetContainer: HTMLElement | null | undefined +}) { + useEffect(() => { + const ready = target && targetContainer + + if (!ready) return + + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + if (entry.contentBoxSize) { + const newWidth = entry.contentRect.width + + target.style.width = `${newWidth}px` + } + } + }) + + resizeObserver.observe(targetContainer) + + return () => { + resizeObserver.unobserve(targetContainer) + } + }, [target, targetContainer]) +} + +export function useStickyNested({ + scrollableArea, + target, + targetContainer, + beacon, + offset = 0, +}: { + scrollableArea: HTMLElement | null | undefined + beacon: HTMLElement | null | undefined + target: HTMLElement | null + targetContainer: HTMLElement | null | undefined + offset?: number +}) { + useEffect(() => { + const ready = scrollableArea && target && beacon + if (!ready) return + + const handleScroll = () => { + const containerRect = scrollableArea.getBoundingClientRect() + const beaconRect = beacon.getBoundingClientRect() + const targetRect = target.getBoundingClientRect() + const targetWidth = targetRect.width + const top = containerRect.top + offset + + let constrainedHeight = containerRect.bottom - beaconRect.top - offset + + if (top >= beaconRect.top) { + constrainedHeight = + containerRect.bottom - containerRect.top - offset * 2 + target.style.position = 'fixed' + target.style.top = `${top}px` + target.style.width = `${targetWidth}px` + } else if (containerRect.top > 0) { + target.style.position = 'relative' + target.style.width = `${targetWidth}px` + target.style.top = '' + } + target.style.maxHeight = `${constrainedHeight}px` + } + + scrollableArea.addEventListener('scroll', handleScroll) + + handleScroll() + + return () => { + scrollableArea.removeEventListener('scroll', handleScroll) + } + }, [scrollableArea, beacon, target, offset]) + + useUpdateWidthOnTargetContainerChange({ target, targetContainer }) +} diff --git a/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx b/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx index 49e696212..b33a2347c 100644 --- a/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx +++ b/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx @@ -43,7 +43,7 @@ const DialogContent = forwardRef< ref={ref} forceMount={forceMount} className={cn( - 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 max-h-[90%] overflow-y-auto border dark:border bg-background p-6 shadow duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', + 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 max-h-[90%] overflow-y-auto custom-scrollbar border dark:border bg-background p-6 shadow duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', _cn, )} {...props} diff --git a/packages/web-ui/src/ds/atoms/SplitPane/index.tsx b/packages/web-ui/src/ds/atoms/SplitPane/index.tsx index f3a9e7945..82ee33d2f 100644 --- a/packages/web-ui/src/ds/atoms/SplitPane/index.tsx +++ b/packages/web-ui/src/ds/atoms/SplitPane/index.tsx @@ -6,6 +6,7 @@ import { RefObject, SyntheticEvent, useCallback, + useEffect, useState, } from 'react' @@ -13,8 +14,27 @@ import { ResizableBox, ResizeCallbackData, ResizeHandle } from 'react-resizable' import { cn } from '../../../lib/utils' +const JS_PANEL_CLASS = 'js-pane' function Pane({ children }: { children: ReactNode }) { - return
{children}
+ return
{children}
+} + +export function usePanelDomRef({ + selfRef, +}: { + selfRef: HTMLElement | null | undefined +}) { + const [panelRef, setPanelRef] = useState( + undefined, + ) + useEffect(() => { + if (!selfRef) return + + const pane = selfRef.closest(`.${JS_PANEL_CLASS}`) as HTMLDivElement + setPanelRef(pane) + }, [selfRef]) + + return panelRef } const PaneWrapper = ({ @@ -31,6 +51,7 @@ const PaneWrapper = ({
( maxHeight: maxHeight && typeof maxHeight === 'number' ? `${maxHeight}px` - : 'auto', + : 'unset', }} className='flex flex-col relative w-full rounded-lg border overflow-hidden' > diff --git a/packages/web-ui/src/ds/molecules/ListingHeader/index.tsx b/packages/web-ui/src/ds/molecules/ListingHeader/index.tsx index 69dfc9322..f9eb01032 100644 --- a/packages/web-ui/src/ds/molecules/ListingHeader/index.tsx +++ b/packages/web-ui/src/ds/molecules/ListingHeader/index.tsx @@ -1,5 +1,6 @@ import { ButtonHTMLAttributes, ReactNode } from 'react' +import { cn } from '../../../lib/utils' import { Button } from '../../atoms' import { TitleWithActions } from '../TitleWithActions' @@ -18,15 +19,27 @@ export const TableWithHeader = ({ title, actions, table, + takeVertialSpace, }: { title: string | ReactNode actions?: ReactNode table?: ReactNode + takeVertialSpace?: boolean }) => { return ( -
+
-
{table}
+
+ {table} +
) }