From daec81884c3b715c380caf9e673b465dd14d874b Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 8 Apr 2026 21:10:45 -0500 Subject: [PATCH] Restore row-click navigation, add image lightbox, refine OCR prompts - Change cell copy from click-to-copy to a small hover button so clicking a table row navigates to the card detail page again - Add fullscreen lightbox popup on card detail page images with Escape to close and click-outside dismiss - Further strengthen nextStep OCR prompts to emphasize inspecting each checkbox square independently and note most respondents check only one Made-with: Cursor --- src/app/cards/[id]/page.tsx | 52 ++++++++++++++++++++++++++++- src/components/cards/data-table.tsx | 38 ++++++++------------- src/lib/ai-ocr.ts | 10 +++--- src/lib/ollama.ts | 8 ++--- 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src/app/cards/[id]/page.tsx b/src/app/cards/[id]/page.tsx index 7b79189..b0b89a7 100644 --- a/src/app/cards/[id]/page.tsx +++ b/src/app/cards/[id]/page.tsx @@ -24,6 +24,8 @@ import { Clock, Monitor, LayoutGrid, + Maximize2, + X, } from "lucide-react"; import { Header } from "@/components/layout/header"; @@ -647,18 +649,66 @@ export default function CardDetailPage() { } function ImagePanel({ label, url }: { label: string; url: string | null }) { + const [lightboxOpen, setLightboxOpen] = React.useState(false); + + React.useEffect(() => { + if (!lightboxOpen) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setLightboxOpen(false); + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [lightboxOpen]); + return (
{url ? ( -
+
{label} +
) : (
)} + + {lightboxOpen && url && ( +
setLightboxOpen(false)} + > + +
e.stopPropagation()} + > + {label} +
+
+ )}
); } diff --git a/src/components/cards/data-table.tsx b/src/components/cards/data-table.tsx index a7f9fe2..32e454d 100644 --- a/src/components/cards/data-table.tsx +++ b/src/components/cards/data-table.tsx @@ -239,28 +239,17 @@ export function DataTable({ target.closest("button") || target.closest("input") || target.closest('[data-slot="checkbox"]') || - target.closest('[data-slot="dropdown-menu"]') || - target.closest("[data-copy-cell]") + target.closest('[data-slot="dropdown-menu"]') ) { return; } router.push(`/cards/${rowId}`); }; - const handleCellClick = React.useCallback( - async (e: React.MouseEvent, cellId: string, columnId: string) => { - if (NON_COPYABLE_COLUMNS.has(columnId)) return; - - if ( - (e.target as HTMLElement).closest("button") || - (e.target as HTMLElement).closest("input") || - (e.target as HTMLElement).closest('[data-slot="checkbox"]') - ) { - return; - } - + const handleCopyClick = React.useCallback( + async (e: React.MouseEvent, cellElement: HTMLTableCellElement, cellId: string) => { e.stopPropagation(); - const text = getCellText(e.currentTarget); + const text = getCellText(cellElement); if (!text || text === "—") return; try { @@ -418,29 +407,30 @@ export function DataTable({ const isSticky = colId === "select" || !!colMeta?.sticky; const cellKey = `${row.id}_${colId}`; const isCopied = copiedCellId === cellKey; + const cellRef = React.createRef(); return ( handleCellClick(e, cellKey, colId) - : undefined - } > {flexRender( cell.column.columnDef.cell, cell.getContext() )} {isCopyable && ( - + )} ); diff --git a/src/lib/ai-ocr.ts b/src/lib/ai-ocr.ts index 972b0a0..9b434fd 100644 --- a/src/lib/ai-ocr.ts +++ b/src/lib/ai-ocr.ts @@ -63,7 +63,7 @@ const surveySchema = z.object({ ), messageTopicsOther: z.string().nullable().describe("Value if Other is filled in"), nextStep: z.array(z.string()).describe( - "This question has exactly 2 checkboxes. Include ONLY the ones with a visible mark (X, checkmark, filled). 'Baptism' = the checkbox for 'Expressing my faith in Jesus / baptized'. 'Next Steps' = the checkbox for 'Learning more about becoming a partner / attend Next Steps'. Return empty array if neither checkbox is marked." + "CRITICAL: This question has EXACTLY 2 separate checkboxes, each on its own line. Inspect each checkbox square individually. 'Baptism' = FIRST checkbox (next to 'Expressing my faith in Jesus...'). 'Next Steps' = SECOND checkbox (next to 'Learning more about becoming a partner...'). A checkbox is checked ONLY if its square contains a visible X, checkmark, or fill. An empty square = NOT checked. Most people check only one. Return empty array if neither is marked." ), attendanceDuration: z.string().nullable().describe( "The single checked radio option. One of: Less than 6 months, 6 Months - 1 Year, 1-3 Years, 4-6 Years, or 7+ Years. null if none is marked." @@ -97,12 +97,14 @@ const SURVEY_SYSTEM_PROMPT = `You are analyzing a scanned church Easter survey f Extract ALL of the following fields from the image. CHECKBOX RULES - be very strict: -- A checkbox is CHECKED only if it has a visible mark inside it: an X, a checkmark, a filled square, or pen/pencil marks inside the box. -- An EMPTY box (no marks inside) means UNCHECKED, even if text appears next to it. +- A checkbox is CHECKED only if its square box contains a visible mark: an X, a checkmark, a filled square, or pen/pencil marks INSIDE the box. +- An EMPTY box (no marks inside the square) means UNCHECKED, even if text appears next to it. - When in doubt, treat a checkbox as UNCHECKED. -- For array fields (messageTopics, nextStep, campusPreference, howHeard): ONLY include items whose checkbox is physically marked. Return empty arrays when no checkboxes in that group are marked. +- For array fields (messageTopics, nextStep, campusPreference, howHeard): ONLY include items whose checkbox square is physically marked. Return empty arrays when no checkboxes in that group are marked. - Do NOT confuse section headings or question titles with checked answers. +SPECIAL ATTENTION for "Next Step" question (question 2): It has exactly 2 checkboxes on separate lines. Look at each checkbox square independently. Most respondents check only one. Do not assume both are checked. + Be precise: return null for fields you cannot read.`; export interface OcrResult { diff --git a/src/lib/ollama.ts b/src/lib/ollama.ts index 0c134b5..4bd0be9 100644 --- a/src/lib/ollama.ts +++ b/src/lib/ollama.ts @@ -64,10 +64,10 @@ CHECKBOX RULES - be very strict: - For array fields: ONLY include items whose checkbox is physically marked. Return empty arrays when no checkboxes in that group are marked. - Do NOT confuse section headings or question titles with checked answers. -IMPORTANT for nextStep: There are exactly 2 checkboxes. -- "Baptism" = the checkbox for "Expressing my faith in Jesus / I want to be baptized..." -- "Next Steps" = the checkbox for "Learning more about becoming a partner / attend Next Steps..." -Only include the one(s) whose box is physically marked. +SPECIAL ATTENTION for nextStep: There are exactly 2 checkboxes on separate lines. +- "Baptism" = FIRST checkbox (next to "Expressing my faith in Jesus / I want to be baptized...") +- "Next Steps" = SECOND checkbox (next to "Learning more about becoming a partner / attend Next Steps...") +Inspect each checkbox square independently. A checkbox is checked ONLY if its square contains a visible X, checkmark, or fill. An empty square = NOT checked. Most respondents check only one. Do NOT assume both are checked. Return ONLY valid JSON with this exact structure (no markdown, no code fences): {