wulf-pulse/app/analyzer/ticket/[ticketNumber]/page.tsx

131 lines
4.7 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useState, use } from 'react';
import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AnalyzeButton } from '@/components/analyzer/analyze-button';
import { Sparkles } from 'lucide-react';
import type { PersistedAnalysis } from '@/lib/types/analyzer';
export default function TicketAnalyzerPage({
params,
}: {
params: Promise<{ ticketNumber: string }>;
}) {
const { ticketNumber } = use(params);
const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch(
`/api/analyzer/tickets/${encodeURIComponent(ticketNumber)}/analyses`
);
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(data.error ?? `Request failed: ${res.status}`);
}
const { analyses } = (await res.json()) as { analyses: PersistedAnalysis[] };
if (!cancelled) setAnalyses(analyses);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : 'Unknown error');
}
})();
return () => {
cancelled = true;
};
}, [ticketNumber]);
const latest = analyses?.[0];
return (
<div className="container mx-auto px-6 py-6 max-w-4xl space-y-6">
<Card>
<CardHeader>
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="space-y-1">
<p className="text-sm text-muted-foreground">Ticket</p>
<CardTitle className="font-mono">{ticketNumber}</CardTitle>
</div>
<AnalyzeButton ticketNumber={ticketNumber} />
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Click <strong>Analyze</strong> to run the AI pipeline. If a current
analysis already exists, you&rsquo;ll be navigated straight to it.
Otherwise the run takes ~1060 seconds.
</p>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertTitle>Couldn&rsquo;t load analysis history</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<CardTitle className="text-base">Analysis history</CardTitle>
</CardHeader>
<CardContent>
{analyses === null && !error ? (
<div className="space-y-2">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
) : analyses && analyses.length === 0 ? (
<p className="text-sm text-muted-foreground">
No analyses yet. Run one above.
</p>
) : (
<ul className="divide-y">
{(analyses ?? []).map((a) => (
<li key={a.id} className="py-3 flex items-center justify-between gap-4">
<div className="min-w-0">
<Link
href={`/analyzer/analysis/${a.id}`}
className="font-medium hover:underline flex items-center gap-2"
>
<Sparkles className="w-4 h-4" />
Version {a.analysisVersion}
{latest?.id === a.id && (
<Badge variant="secondary" className="text-xs">
latest
</Badge>
)}
{a.needsHumanReview && (
<Badge variant="destructive" className="text-xs">
Needs review
</Badge>
)}
</Link>
<p className="text-xs text-muted-foreground mt-1">
{new Date(a.triggeredAt).toLocaleString()}
{' · '}
{a.opusUsed ? 'Haiku → Sonnet → Opus' : a.sonnetUsed ? 'Haiku → Sonnet' : 'Haiku'}
{' · '}${a.estimatedCostUsd.toFixed(4)}
</p>
</div>
{a.confidenceScore !== null && (
<Badge variant="outline">
{Math.round(a.confidenceScore * 100)}%
</Badge>
)}
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}