From 9c1a74009bb2303b09d998df059c678880dc6399 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 21:30:17 -0400 Subject: [PATCH] feat(06-02): add AnalyzerStagePips, ConfidenceBadge, AnalyzerRowSkeleton, AnalyzerFeedRow components - AnalyzerStagePips: 3-dot stage indicator with caret separators, sr-only accessibility label - ConfidenceBadge: High/Medium/Low buckets (0.85/0.65) with green/amber/slate tones, dark mode - AnalyzerRowSkeleton: Card-wrapped skeleton matching row shape (no border-l-4 per D-11) - AnalyzerFeedRow: Full card row with header/title/summary/footer, Link to /mobile/analyzer/[id] --- components/mobile/AnalyzerFeedRow.tsx | 79 +++++++++++++++++++++++ components/mobile/AnalyzerRowSkeleton.tsx | 29 +++++++++ components/mobile/AnalyzerStagePips.tsx | 36 +++++++++++ components/mobile/ConfidenceBadge.tsx | 39 +++++++++++ 4 files changed, 183 insertions(+) create mode 100644 components/mobile/AnalyzerFeedRow.tsx create mode 100644 components/mobile/AnalyzerRowSkeleton.tsx create mode 100644 components/mobile/AnalyzerStagePips.tsx create mode 100644 components/mobile/ConfidenceBadge.tsx diff --git a/components/mobile/AnalyzerFeedRow.tsx b/components/mobile/AnalyzerFeedRow.tsx new file mode 100644 index 0000000..a73ecbd --- /dev/null +++ b/components/mobile/AnalyzerFeedRow.tsx @@ -0,0 +1,79 @@ +'use client'; + +/* AnalyzerFeedRow — phase 06 (ANL-02). + * Purpose: Feed row card — header (ticket# + time-ago), title (1-line truncate), + * summary (2-line clamp), footer (stage pips left + confidence/review right). + * Entire card is a Link to /mobile/analyzer/[id]. Per D-10..D-12, D-17. + * Props: AnalyzerFeedRow type from @/app/api/mobile/analyzer/feed/route. */ + +import Link from 'next/link'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { AnalyzerStagePips } from '@/components/mobile/AnalyzerStagePips'; +import { ConfidenceBadge } from '@/components/mobile/ConfidenceBadge'; +import type { AnalyzerFeedRow as AnalyzerFeedRowType } from '@/app/api/mobile/analyzer/feed/route'; + +function relTime(ts: string | null): string { + if (!ts) return '—'; + const diff = Date.now() - new Date(ts).getTime(); + const m = Math.floor(diff / 60000); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + return `${Math.floor(h / 24)}d ago`; +} + +export interface AnalyzerFeedRowProps { + row: AnalyzerFeedRowType; +} + +export function AnalyzerFeedRow({ row }: AnalyzerFeedRowProps) { + return ( + + + + {/* Line 1 — header row */} +
+ + {row.ticketNumber} + + + {relTime(row.completedAt)} + +
+ + {/* Line 2 — title */} +

+ {row.title} +

+ + {/* Line 3 — summary clamp (2 lines, fallback "—") */} +

+ {row.summary ?? '—'} +

+ + {/* Footer — pips left, badges right */} +
+ +
+ + {row.needsHumanReview && ( + + Review + + )} +
+
+
+
+ + ); +} diff --git a/components/mobile/AnalyzerRowSkeleton.tsx b/components/mobile/AnalyzerRowSkeleton.tsx new file mode 100644 index 0000000..32896a4 --- /dev/null +++ b/components/mobile/AnalyzerRowSkeleton.tsx @@ -0,0 +1,29 @@ +'use client'; + +/* AnalyzerRowSkeleton — phase 06 (D-28). + * Purpose: Skeleton placeholder matching analyzer feed row shape (no priority stripe per D-11). + * Renders 5 instances on initial load. + * Props: none — purely presentational. */ + +import { Card, CardContent } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; + +export function AnalyzerRowSkeleton() { + return ( + + +
+ + +
+ + + +
+ + +
+
+
+ ); +} diff --git a/components/mobile/AnalyzerStagePips.tsx b/components/mobile/AnalyzerStagePips.tsx new file mode 100644 index 0000000..d311676 --- /dev/null +++ b/components/mobile/AnalyzerStagePips.tsx @@ -0,0 +1,36 @@ +'use client'; + +/* AnalyzerStagePips — phase 06 (ANL-02). + * Purpose: 3-dot stage indicator (Triage → Analyze → Deep Review) with caret separators. + * Filled when stage was used, muted when not. Per D-13/D-14 (no animation). + * Props: see AnalyzerStagePipsProps. */ + +export interface AnalyzerStagePipsProps { + haikuUsed: boolean; + sonnetUsed: boolean; + opusUsed: boolean; +} + +const STAGE_LABELS = ['Triage', 'Analyze', 'Deep Review']; + +export function AnalyzerStagePips({ haikuUsed, sonnetUsed, opusUsed }: AnalyzerStagePipsProps) { + const used = [haikuUsed, sonnetUsed, opusUsed]; + const completed = STAGE_LABELS.filter((_, i) => used[i]); + const srLabel = completed.length === 0 + ? 'No stages completed' + : `Stages completed: ${completed.join(', ')}`; + return ( +
+ {srLabel} + {used.map((isUsed, i) => ( + + + ))} +
+ ); +} diff --git a/components/mobile/ConfidenceBadge.tsx b/components/mobile/ConfidenceBadge.tsx new file mode 100644 index 0000000..c44ec92 --- /dev/null +++ b/components/mobile/ConfidenceBadge.tsx @@ -0,0 +1,39 @@ +'use client'; + +/* ConfidenceBadge — phase 06 (ANL-02). + * Purpose: Bucketed confidence label — High (>=0.85) / Medium (>=0.65) / Low (<0.65). + * Renders nothing when score is null. Per D-15 / D-16. + * Props: see ConfidenceBadgeProps. */ + +import { Badge } from '@/components/ui/badge'; + +export interface ConfidenceBadgeProps { + score: number | null; +} + +export function ConfidenceBadge({ score }: ConfidenceBadgeProps) { + if (score === null) return null; + + let label: string; + let className: string; + if (score >= 0.85) { + label = 'High'; + className = 'bg-green-500/10 text-green-700 dark:text-green-400'; + } else if (score >= 0.65) { + label = 'Medium'; + className = 'bg-amber-500/10 text-amber-700 dark:text-amber-400'; + } else { + label = 'Low'; + className = 'bg-slate-500/10 text-slate-600 dark:text-slate-400'; + } + + return ( + + {label} + + ); +}