diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..14bb6db --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,89 @@ +name: PR Build Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: "pr-check-${{ github.event.number }}" + cancel-in-progress: true + +jobs: + build-check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + lfs: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Copy static assets + working-directory: frontend + run: | + mkdir -p public/thumbnails public/downsampled public/annotations/nsd + cp -r ../data/thumbnails/* public/thumbnails/ 2>/dev/null || echo "No thumbnails to copy" + cp -r ../images/downsampled/* public/downsampled/ 2>/dev/null || echo "No images to copy" + cp -r ../annotations/nsd/*.json public/annotations/nsd/ 2>/dev/null || echo "No annotations to copy" + + - name: Build Next.js site + working-directory: frontend + run: npm run build + + - name: Verify build output + run: | + echo "Build completed successfully!" + echo "Output files:" + ls -la frontend/out/ + echo "" + echo "Static assets:" + ls -la frontend/out/thumbnails/ 2>/dev/null | head -5 || echo "No thumbnails" + ls -la frontend/out/downsampled/ 2>/dev/null | head -5 || echo "No downsampled images" + ls -la frontend/out/annotations/nsd/ 2>/dev/null | head -5 || echo "No annotations" + + - name: Comment PR with build status + uses: actions/github-script@v7 + with: + script: | + const body = `## Build Check Passed\n\nThe frontend build completed successfully. Once merged, changes will deploy to: https://annotation-garden.github.io/image-annotation/`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && comment.body.includes('Build Check') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } diff --git a/.gitignore b/.gitignore index d4b1940..c320b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -112,11 +112,12 @@ ehthumbs.db test_outputs/ test_results/ -# Frontend public assets (copied for deployment) +# Frontend public assets (copied for deployment, except essentials) frontend/public/* !frontend/public/AGI-square.svg !frontend/public/AGI-square.png !frontend/public/favicon.ico +!frontend/public/image-list.json # Image files (but keep README, and downsampled) images/original/*.png diff --git a/frontend/app/components/AnnotationViewer.tsx b/frontend/app/components/AnnotationViewer.tsx index 9eb7c89..335db87 100644 --- a/frontend/app/components/AnnotationViewer.tsx +++ b/frontend/app/components/AnnotationViewer.tsx @@ -13,10 +13,10 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) const [viewMode, setViewMode] = useState<'text' | 'json'>('text') const handleCopy = () => { - const textToCopy = viewMode === 'json' + const textToCopy = viewMode === 'json' ? JSON.stringify(annotation.response_data || annotation.response, null, 2) : annotation.response - + navigator.clipboard.writeText(textToCopy) setCopied(true) setTimeout(() => setCopied(false), 2000) @@ -34,8 +34,8 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) return (
+)} @@ -144,44 +144,44 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) {(annotation.token_metrics || annotation.performance_metrics) && ({JSON.stringify(annotation.response, null, 2)}{annotation.token_metrics && ( -+)} {annotation.performance_metrics && ( --- Token Usage + + Token Usage - Input: - {annotation.token_metrics.input_tokens} + Input: + {annotation.token_metrics.input_tokens}- Output: - {annotation.token_metrics.output_tokens} + Output: + {annotation.token_metrics.output_tokens}- Total: - {annotation.token_metrics.total_tokens} + Total: + {annotation.token_metrics.total_tokens}+-- Performance + + Performance ) -} \ No newline at end of file +} diff --git a/frontend/app/components/ThumbnailRibbon.tsx b/frontend/app/components/ThumbnailRibbon.tsx index 32a79ec..362e2a3 100644 --- a/frontend/app/components/ThumbnailRibbon.tsx +++ b/frontend/app/components/ThumbnailRibbon.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRef, useEffect } from 'react' +import { useRef, useEffect, useState, useCallback } from 'react' import { ImageData } from '../types' import { ChevronLeft, ChevronRight } from 'lucide-react' @@ -13,17 +13,19 @@ interface ThumbnailRibbonProps { export default function ThumbnailRibbon({ images, selectedIndex, onSelect }: ThumbnailRibbonProps) { const scrollContainerRef = useRef- Speed: - + Speed: + {annotation.performance_metrics.tokens_per_second.toFixed(1)} t/s- Time: - + Time: + {(annotation.performance_metrics.total_duration_ms / 1000).toFixed(2)}s@@ -192,4 +192,4 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) )}(null) const thumbnailRefs = useRef<(HTMLButtonElement | null)[]>([]) + const progressBarRef = useRef (null) + const [isDragging, setIsDragging] = useState(false) // Scroll to selected thumbnail when it changes useEffect(() => { if (thumbnailRefs.current[selectedIndex] && scrollContainerRef.current) { const thumbnail = thumbnailRefs.current[selectedIndex] const container = scrollContainerRef.current - + if (thumbnail) { const containerWidth = container.clientWidth const scrollLeft = thumbnail.offsetLeft - containerWidth / 2 + thumbnail.clientWidth / 2 - + container.scrollTo({ left: scrollLeft, behavior: 'smooth' @@ -58,16 +60,57 @@ export default function ThumbnailRibbon({ images, selectedIndex, onSelect }: Thu } } + // Handle click/drag on progress bar to jump to position + const handleProgressInteraction = useCallback((clientX: number) => { + if (!progressBarRef.current || images.length === 0) return + + const rect = progressBarRef.current.getBoundingClientRect() + const relativeX = Math.max(0, Math.min(clientX - rect.left, rect.width)) + const percentage = relativeX / rect.width + const newIndex = Math.min( + Math.floor(percentage * images.length), + images.length - 1 + ) + onSelect(newIndex) + }, [images.length, onSelect]) + + const handleMouseDown = (e: React.MouseEvent) => { + setIsDragging(true) + handleProgressInteraction(e.clientX) + } + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (isDragging) { + handleProgressInteraction(e.clientX) + } + }, [isDragging, handleProgressInteraction]) + + const handleMouseUp = useCallback(() => { + setIsDragging(false) + }, []) + + // Add/remove mouse event listeners for drag + useEffect(() => { + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', handleMouseUp) + } + return () => { + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', handleMouseUp) + } + }, [isDragging, handleMouseMove, handleMouseUp]) + return ( - +{/* Left scroll button */} {/* Thumbnail container */} @@ -78,13 +121,13 @@ export default function ThumbnailRibbon({ images, selectedIndex, onSelect }: Thu tabIndex={0} style={{ scrollbarWidth: 'thin', - scrollbarColor: 'rgb(147 51 234 / 0.3) transparent' + scrollbarColor: 'rgba(24, 74, 61, 0.3) transparent' }} > {images.map((image, index) => { // Extract the image number for display const imageNumber = image.id.match(/shared(\d+)/)?.[1] || String(index + 1) - + return (