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 (
{data.map((item, index) => ( -
-
Item {index + 1}
+
+
Item {index + 1}
{renderJsonData(item)}
))} @@ -49,15 +49,15 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps)
{Object.entries(data).map(([key, value]) => (
- + {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: {typeof value === 'object' ? ( -
+
{renderJsonData(value)}
) : ( - + {String(value)} )} @@ -68,15 +68,15 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) } // Primitive value - return {String(data)} + return {String(data)} } return (
{/* Prompt Text */} -
-
Prompt
-
+
+
Prompt
+
{annotation.prompt_text}
@@ -88,8 +88,8 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) onClick={() => setViewMode('text')} className={`px-3 py-1.5 rounded-lg text-sm flex items-center gap-1.5 transition-all ${ viewMode === 'text' - ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30' - : 'bg-gray-800/30 text-gray-400 hover:bg-gray-800/50 border border-gray-700/30' + ? 'bg-agi-teal/10 text-agi-teal border border-agi-teal/30' + : 'bg-stone-100 text-agi-teal-600 hover:bg-stone-200 border border-stone-200' }`} > @@ -99,8 +99,8 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) onClick={() => setViewMode('json')} className={`px-3 py-1.5 rounded-lg text-sm flex items-center gap-1.5 transition-all ${ viewMode === 'json' - ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30' - : 'bg-gray-800/30 text-gray-400 hover:bg-gray-800/50 border border-gray-700/30' + ? 'bg-agi-teal/10 text-agi-teal border border-agi-teal/30' + : 'bg-stone-100 text-agi-teal-600 hover:bg-stone-200 border border-stone-200' }`} > @@ -113,19 +113,19 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps)
-
+
{viewMode === 'text' ? ( -
+
{annotation.response}
) : hasJsonData ? ( @@ -133,7 +133,7 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) {renderJsonData(annotation.response_data)}
) : ( -
+            
               {JSON.stringify(annotation.response, null, 2)}
             
)} @@ -144,44 +144,44 @@ export default function AnnotationViewer({ annotation }: AnnotationViewerProps) {(annotation.token_metrics || annotation.performance_metrics) && (
{annotation.token_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}
)} {annotation.performance_metrics && ( -
+
- - Performance + + Performance
- 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) )}
) -} \ 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(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 (
- - {/* Progress indicator */} -
-
- {Array.from({ length: Math.min(10, Math.ceil(images.length / 10)) }).map((_, i) => ( -
- ))} + + {/* Progress indicator - clickable/draggable slider */} +
+
+ {/* Track background */} +
+ + {/* Filled track */} +
+ + {/* Thumb/handle */} +
+ + {/* Segment markers */} +
+ {Array.from({ length: 11 }).map((_, i) => ( +
+ ))} +
- + + {selectedIndex + 1} / {images.length}
) -} \ No newline at end of file +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index e4d4a61..fe68d05 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' import ThumbnailRibbon from './components/ThumbnailRibbon' import AnnotationViewer from './components/AnnotationViewer' import { ImageData, Annotation, PromptAnnotation } from './types' -import { Brain, Sparkles, ChevronDown, Loader2, ExternalLink } from 'lucide-react' +import { Sparkles, ChevronDown, Loader2, ExternalLink } from 'lucide-react' export default function Dashboard() { const [images, setImages] = useState([]) @@ -129,31 +129,33 @@ export default function Dashboard() { if (loading) { return ( -
+
- -
Loading neural interface...
+ +
Loading annotation interface...
) } return ( -
+
{/* Header */} -
+
-
- -
-

- The Annotation Garden Project + AGI Logo +

+ Annotation Garden

-
- - AI-Powered Vision Analysis +
+ + Collaborative Image Annotation
@@ -162,10 +164,10 @@ export default function Dashboard() {
{/* Image Viewer - Full width on mobile, constrained on desktop */}
-
+
{imageLoading && ( -
- +
+
)} {images[selectedImageIndex] && ( @@ -180,15 +182,15 @@ export default function Dashboard() {
{/* Image Info Bar */} -
+
- Image ID: - + Image ID: + {images[selectedImageIndex]?.id || 'Loading...'}
-
+
{selectedImageIndex + 1} / {images.length}
@@ -198,8 +200,8 @@ export default function Dashboard() { {/* Controls and Annotations - Full width on mobile, side panel on desktop */}
{/* Model Selection */} -
-