File size: 2,488 Bytes
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"use client";
import { useState } from "react";
import {
  ChevronRight, CheckCircle2, AlertTriangle, XCircle, MinusCircle, Loader2, Circle,
  type LucideIcon,
} from "lucide-react";
import type { TraceStep } from "@/lib/api";

const STATUS: Record<TraceStep["status"], { icon: LucideIcon; cls: string }> = {
  success: { icon: CheckCircle2, cls: "text-ok" },
  warning: { icon: AlertTriangle, cls: "text-warn" },
  failed: { icon: XCircle, cls: "text-bad" },
  skipped: { icon: MinusCircle, cls: "text-fg3" },
  running: { icon: Loader2, cls: "text-brand animate-spin" },
  pending: { icon: Circle, cls: "text-fg3" },
};

/**
 * Collapsed-by-default reasoning trace shown under an answer (Perplexity Pro
 * Search pattern): don't overload until the user is curious, then reveal the
 * per-step plan + timings on demand. Full detail still lives in the Trace tab.
 */
export function AnswerSteps({ steps }: { steps: TraceStep[] }) {
  const [open, setOpen] = useState(false);
  if (!steps?.length) return null;
  const total = steps.reduce((a, s) => a + (s.duration_ms || 0), 0);

  return (
    <div className="mt-2">
      <button
        type="button"
        onClick={() => setOpen((o) => !o)}
        aria-expanded={open}
        className="inline-flex items-center gap-1.5 text-xs text-fg3 transition hover:text-fg2"
      >
        <ChevronRight className={`h-3.5 w-3.5 transition-transform ${open ? "rotate-90" : ""}`} aria-hidden />
        {open ? "Hide" : "Show"} reasoning
        <span className="font-mono text-fg3">· {steps.length} steps · {Math.round(total)}ms</span>
      </button>
      {open && (
        <ol className="mt-1.5 space-y-1 border-l border-edge pl-3 duration-200 animate-in fade-in-0 slide-in-from-top-1">
          {steps.map((s) => {
            const st = STATUS[s.status] ?? STATUS.pending;
            const Icon = st.icon;
            return (
              <li key={s.id} className="flex items-center gap-2 text-xs">
                <Icon className={`h-3 w-3 shrink-0 ${st.cls}`} aria-hidden />
                <span className="truncate text-fg2">{s.label || s.name}</span>
                {s.evidence_count != null && s.evidence_count > 0 && (
                  <span className="shrink-0 text-fg3">· {s.evidence_count} ev</span>
                )}
                <span className="ml-auto shrink-0 font-mono text-fg3">{Math.round(s.duration_ms)}ms</span>
              </li>
            );
          })}
        </ol>
      )}
    </div>
  );
}