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
This commit is contained in:
parent
13fb94fee9
commit
daec81884c
4 changed files with 75 additions and 33 deletions
|
|
@ -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 (
|
||||
<div>
|
||||
<Label className="mb-1.5 block text-xs font-medium text-muted-foreground">{label}</Label>
|
||||
{url ? (
|
||||
<div className="relative aspect-[3/4] overflow-hidden rounded-xl border border-border/50 bg-muted/30">
|
||||
<div className="group relative aspect-[3/4] overflow-hidden rounded-xl border border-border/50 bg-muted/30">
|
||||
<Image src={url} alt={label} fill className="object-contain" unoptimized />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLightboxOpen(true)}
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/0 opacity-0 transition-all group-hover:bg-black/20 group-hover:opacity-100"
|
||||
>
|
||||
<div className="rounded-full bg-background/90 p-2 shadow-lg">
|
||||
<Maximize2 className="size-4 text-foreground" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex aspect-[3/4] items-center justify-center rounded-xl border border-border/50 bg-muted/20">
|
||||
<ImageIcon className="size-12 text-muted-foreground/20" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lightboxOpen && url && (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in-0 duration-200"
|
||||
onClick={() => setLightboxOpen(false)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLightboxOpen(false)}
|
||||
className="absolute right-4 top-4 z-10 rounded-full bg-background/20 p-2 text-white transition-colors hover:bg-background/40"
|
||||
>
|
||||
<X className="size-6" />
|
||||
</button>
|
||||
<div
|
||||
className="relative max-h-[90vh] max-w-[90vw]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Image
|
||||
src={url}
|
||||
alt={label}
|
||||
width={1200}
|
||||
height={1600}
|
||||
className="max-h-[90vh] w-auto rounded-lg object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,28 +239,17 @@ export function DataTable<TData extends ResponseCard>({
|
|||
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<HTMLTableCellElement>, 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<TData extends ResponseCard>({
|
|||
const isSticky = colId === "select" || !!colMeta?.sticky;
|
||||
const cellKey = `${row.id}_${colId}`;
|
||||
const isCopied = copiedCellId === cellKey;
|
||||
const cellRef = React.createRef<HTMLTableCellElement>();
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
data-copy-cell={isCopyable ? "true" : undefined}
|
||||
ref={cellRef}
|
||||
className={cn(
|
||||
"px-4 py-3 relative group/cell",
|
||||
isCopyable && "cursor-cell",
|
||||
isSticky && "sticky left-0 z-10 bg-background",
|
||||
isCopied && "ring-2 ring-primary/40 ring-inset"
|
||||
)}
|
||||
onClick={
|
||||
isCopyable
|
||||
? (e) => handleCellClick(e, cellKey, colId)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
{isCopyable && (
|
||||
<span className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover/cell:opacity-100 transition-opacity p-0.5 rounded hover:bg-muted"
|
||||
onClick={(e) => {
|
||||
if (cellRef.current) handleCopyClick(e, cellRef.current, cellKey);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 bg-primary/10 text-primary">
|
||||
Copied
|
||||
|
|
@ -448,7 +438,7 @@ export function DataTable<TData extends ResponseCard>({
|
|||
) : (
|
||||
<Copy className="size-3 text-muted-foreground/50" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue