Compare commits
2 Commits
47697b4421
...
3d04995f97
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d04995f97 | |||
| d3069e1f25 |
@@ -262,6 +262,7 @@ export function TicketAssignmentsCard({
|
||||
{videoId ? (
|
||||
<div className={'mt-4 border-t pt-4'}>
|
||||
<TicketDetectionPreview
|
||||
key={`${videoId}:${selectedAssignment.id}`}
|
||||
ticketId={ticketId}
|
||||
videoId={videoId}
|
||||
assignmentId={selectedAssignment.id}
|
||||
|
||||
@@ -24,6 +24,8 @@ interface TicketDetectionPreviewProps {
|
||||
onDetectionCountChange?: (count: number | undefined) => void;
|
||||
}
|
||||
|
||||
const DETECTION_PAGE_SIZE = 10;
|
||||
|
||||
function DetectionMetadataBar({
|
||||
items,
|
||||
}: {
|
||||
@@ -102,6 +104,8 @@ export function TicketDetectionPreview({
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
||||
const isUnassignedPreview = assignmentStatus === 'unassigned';
|
||||
const pageStart =
|
||||
Math.floor(currentIndex / DETECTION_PAGE_SIZE) * DETECTION_PAGE_SIZE;
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentIndex(0);
|
||||
@@ -109,23 +113,26 @@ export function TicketDetectionPreview({
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
skip: currentIndex,
|
||||
limit: 1,
|
||||
skip: pageStart,
|
||||
limit: DETECTION_PAGE_SIZE,
|
||||
assignment_id: isUnassignedPreview ? undefined : assignmentId,
|
||||
assignment_status: isUnassignedPreview
|
||||
? ('unassigned' as const)
|
||||
: undefined,
|
||||
sort: 'timestamp_asc',
|
||||
}),
|
||||
[assignmentId, currentIndex, isUnassignedPreview],
|
||||
[assignmentId, isUnassignedPreview, pageStart],
|
||||
);
|
||||
|
||||
const detectionsQuery = useTicketDetectionsQuery(
|
||||
videoId ?? undefined,
|
||||
queryParams,
|
||||
{ keepPreviousPage: true },
|
||||
);
|
||||
const detectionResult = detectionsQuery.data;
|
||||
const activeDetection = detectionResult?.items[0];
|
||||
const activeDetection = detectionsQuery.isPlaceholderData
|
||||
? undefined
|
||||
: detectionResult?.items[currentIndex - pageStart];
|
||||
const confirmedDetectionCount = detectionResult?.total;
|
||||
const detectionCount = confirmedDetectionCount ?? 0;
|
||||
const activeVisual = getDefectVisual(
|
||||
@@ -156,12 +163,15 @@ export function TicketDetectionPreview({
|
||||
}, [confirmedDetectionCount, currentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
onDetectionCountChange?.(confirmedDetectionCount);
|
||||
}, [confirmedDetectionCount, onDetectionCountChange]);
|
||||
onDetectionCountChange?.(detectionCount);
|
||||
}, [detectionCount, onDetectionCountChange]);
|
||||
|
||||
const isNavigationDisabled =
|
||||
detectionCount === 0 || detectionsQuery.isLoading;
|
||||
const isInitialLoading = detectionsQuery.isLoading;
|
||||
detectionCount === 0 ||
|
||||
detectionsQuery.isLoading ||
|
||||
detectionsQuery.isPlaceholderData;
|
||||
const isInitialLoading =
|
||||
detectionsQuery.isLoading || detectionsQuery.isPlaceholderData;
|
||||
const isError = detectionsQuery.isError;
|
||||
const retry = () => void detectionsQuery.refetch();
|
||||
|
||||
@@ -221,7 +231,7 @@ export function TicketDetectionPreview({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isInitialLoading && !activeDetection ? (
|
||||
{isInitialLoading ? (
|
||||
<TicketDetectionPreviewSkeleton />
|
||||
) : isError ? (
|
||||
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
|
||||
|
||||
@@ -22,6 +22,7 @@ export function TicketDetectionPreviewCard({
|
||||
<CardContent className="p-5">
|
||||
<div className="min-w-0">
|
||||
<TicketDetectionPreview
|
||||
key={`${videoId}:${assignmentId}`}
|
||||
ticketId={ticketId}
|
||||
videoId={videoId}
|
||||
assignmentId={assignmentId}
|
||||
|
||||
@@ -41,12 +41,13 @@ export function TicketUnassignedIssuesCard({
|
||||
results.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={'default'}>{detectionCount ?? 0} Unassigned</Badge>
|
||||
<Badge variant={'default'}>{detectionCount ?? 0} Unassigned</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className={'p-5'}>
|
||||
<div className={'min-w-0'}>
|
||||
<TicketDetectionPreview
|
||||
key={`${videoId}:unassigned`}
|
||||
ticketId={ticketId}
|
||||
videoId={videoId}
|
||||
assignmentStatus={'unassigned'}
|
||||
|
||||
@@ -176,13 +176,9 @@ export function IssueReviewAction({
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => void submitDecision('approve')}
|
||||
className={cn(
|
||||
decision === 'approve' &&
|
||||
'border-emerald-500 bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300',
|
||||
)}
|
||||
>
|
||||
{isPending && decision === 'approve' ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
@@ -193,13 +189,9 @@ export function IssueReviewAction({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => void submitDecision('reject')}
|
||||
className={cn(
|
||||
decision === 'reject' &&
|
||||
'border-destructive bg-destructive/10 text-destructive hover:bg-destructive/15',
|
||||
)}
|
||||
>
|
||||
{isPending && decision === 'reject' ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
@@ -28,6 +29,11 @@ import { extensionRequestKeys } from '../../extension-requests/queries/extension
|
||||
|
||||
const TICKET_TABLE_REFRESH_MS = 5 * 60 * 1000;
|
||||
|
||||
interface TicketDetectionsQueryOptions {
|
||||
enabled?: boolean;
|
||||
keepPreviousPage?: boolean;
|
||||
}
|
||||
|
||||
interface UseTicketsQueryParams {
|
||||
skip: number;
|
||||
limit: number;
|
||||
@@ -113,8 +119,9 @@ export function useTicketAssignmentsQuery(
|
||||
export function useTicketDetectionsQuery(
|
||||
videoId: string | undefined,
|
||||
params: VideoDetectionsParams,
|
||||
enabled = true,
|
||||
options: TicketDetectionsQueryOptions = {},
|
||||
) {
|
||||
const { enabled = true, keepPreviousPage = false } = options;
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
skip: params.skip ?? 0,
|
||||
@@ -143,6 +150,7 @@ export function useTicketDetectionsQuery(
|
||||
queryFn: () =>
|
||||
videoService.getVideoDetections(videoId as string, queryParams),
|
||||
enabled: Boolean(enabled && videoId),
|
||||
placeholderData: keepPreviousPage ? keepPreviousData : undefined,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { getPinnedColumnStyles } from './columnStyles';
|
||||
|
||||
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
||||
return (
|
||||
<ShadTableHeader className="sticky top-0 z-20 bg-muted/70 backdrop-blur supports-backdrop-filter:bg-muted/60">
|
||||
<ShadTableHeader className="sticky top-0 z-20 bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
@@ -39,7 +39,7 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
||||
canSort ? header.column.getToggleSortingHandler() : undefined
|
||||
}
|
||||
className={cn(
|
||||
'h-11 max-w-0 overflow-hidden border-b border-border bg-muted/70 px-4 text-sm font-semibold text-foreground transition-colors',
|
||||
'h-11 max-w-0 overflow-hidden border-b border-border bg-muted px-4 text-sm font-semibold text-foreground transition-colors',
|
||||
canSort && 'cursor-pointer select-none hover:bg-muted',
|
||||
sortDirection && 'bg-muted',
|
||||
)}
|
||||
|
||||
@@ -41,11 +41,7 @@ export default function AnnotatedDetectionImage({
|
||||
]
|
||||
: [];
|
||||
|
||||
const aspectRatio =
|
||||
aspectRatioOverride ??
|
||||
(videoWidth > 0 && videoHeight > 0
|
||||
? `${videoWidth} / ${videoHeight}`
|
||||
: '16 / 9');
|
||||
const aspectRatio = aspectRatioOverride ?? '16 / 9';
|
||||
|
||||
const annotatedMedia = objectUrl ? (
|
||||
<>
|
||||
@@ -75,14 +71,7 @@ export default function AnnotatedDetectionImage({
|
||||
className="group relative flex w-full cursor-zoom-in items-center justify-center overflow-hidden rounded-lg border bg-black focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
style={{ aspectRatio }}
|
||||
>
|
||||
<Image
|
||||
src={objectUrl}
|
||||
alt={`${detection.detection.display_name} detection`}
|
||||
fill
|
||||
unoptimized
|
||||
sizes="(min-width: 1280px) 18rem, (min-width: 640px) 50vw, 100vw"
|
||||
className="object-contain"
|
||||
/>
|
||||
{annotatedMedia}
|
||||
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-[3] bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-8 text-center text-xs font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||
Click to view annotated image
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user