diff --git a/app/mobile/analyzer/[id]/page.tsx b/app/mobile/analyzer/[id]/page.tsx new file mode 100644 index 0000000..9a992d9 --- /dev/null +++ b/app/mobile/analyzer/[id]/page.tsx @@ -0,0 +1,253 @@ +'use client'; + +/* MobileAnalyzerDetailPage — phase 06 (ANL-03, ANL-04, ANL-05). + * Purpose: Per-analysis summary view at /mobile/analyzer/[id]. + * Reads from GET /api/analyzer/analyses/[id] (D-25, reused as-is). + * Renders Summary / Next Step / Next Step Rationale (D-21). + * Provides "View full analysis" footer link to desktop (D-22). + * Read-only — no edit, re-run, cancel, or prompt-tuning controls (D-23, ANL-05). + * Note: Title and company name are NOT rendered — PersistedAnalysis does not include + * those fields (they live on the tickets/companies tables). D-25/D-36 forbid + * modifying the existing endpoint. Breadcrumb conveys ticket identity (D-19). */ + +import { useEffect, useState, use } from 'react'; +import { useRouter } from 'next/navigation'; +import { ArrowLeft, ExternalLink } from 'lucide-react'; +import { toast } from 'sonner'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Separator } from '@/components/ui/separator'; +import { Badge } from '@/components/ui/badge'; +import { AnalyzerStagePips } from '@/components/mobile/AnalyzerStagePips'; +import { ConfidenceBadge } from '@/components/mobile/ConfidenceBadge'; +import type { PersistedAnalysis } from '@/lib/types/analyzer'; + +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`; +} + +interface DetailPageProps { + params: Promise<{ id: string }>; +} + +export default function MobileAnalyzerDetailPage({ params }: DetailPageProps) { + const { id } = use(params); + const router = useRouter(); + + const [analysis, setAnalysis] = useState(null); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + const load = async () => { + setLoading(true); + setNotFound(false); + setError(null); + try { + const r = await fetch(`/api/analyzer/analyses/${encodeURIComponent(id)}`); + if (r.status === 404) { + if (!cancelled) { + setNotFound(true); + setAnalysis(null); + } + return; + } + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const data = await r.json(); + if (!cancelled) setAnalysis(data.analysis as PersistedAnalysis); + } catch (e) { + if (!cancelled) { + const msg = e instanceof Error ? e.message : 'Failed to load analysis'; + setError(msg); + toast.error('Failed to load analysis'); + } + } finally { + if (!cancelled) setLoading(false); + } + }; + void load(); + return () => { cancelled = true; }; + }, [id]); + + // ──── In-page header (D-19) — back chevron + breadcrumb + external link ──── + const header = ( +
+ + + {analysis ? `Analyzer / #${analysis.ticketNumber}` : 'Analyzer'} + + + +
+ ); + + // ──── Loading skeleton (D-28 / UI-SPEC "Detail page loading") ──── + if (loading) { + return ( +
+ {header} +
+ + +
+ + +
+
+ {[0, 1, 2].map((i) => ( +
+ + + + +
+ ))} +
+ ); + } + + // ──── 404 state ──── + if (notFound) { + return ( +
+ {header} +
+

Analysis not found

+ + Back to feed + +
+
+ ); + } + + // ──── Error state ──── + if (error || !analysis) { + return ( +
+ {header} +
+

{error ?? 'Failed to load analysis'}

+ +
+
+ ); + } + + // ──── Loaded — full render ──── + return ( +
+ {header} + + {/* Identity block (D-20 — adjusted: no title/company per interfaces note) */} +
+ + {analysis.ticketNumber} + +

+ {relTime(analysis.completedAt)} +

+
+ + + {analysis.needsHumanReview && ( + + Review + + )} +
+
+ + + + {/* Section 1 — Summary (D-21) */} +
+

Summary

+ {analysis.summary ? ( +

+ {analysis.summary} +

+ ) : ( +

Summary not available.

+ )} +
+ + + + {/* Section 2 — Next Step (D-21) */} +
+

Next Step

+ {analysis.nextStep ? ( +

+ {analysis.nextStep} +

+ ) : ( +

Next step not available.

+ )} +
+ + + + {/* Section 3 — Next Step Rationale (D-21) */} +
+

Next Step Rationale

+ {analysis.nextStepRationale ? ( +

+ {analysis.nextStepRationale} +

+ ) : ( +

Rationale not available.

+ )} +
+ + {/* Footer link (D-22) — "View full analysis" → desktop */} + +
+ ); +}