File size: 3,955 Bytes
c09f67c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { Check, ChevronDown, Copy } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

interface StackTraceProps {
  error?: string;
  stacktrace?: string[];
  className?: string;
}

export function StackTrace({ error, stacktrace, className }: StackTraceProps) {
  const [expanded, setExpanded] = React.useState(false);
  const [copied, setCopied] = React.useState(false);

  if (!error && (!stacktrace || stacktrace.length === 0)) {
    return null;
  }

  const fullText = [error, ...(stacktrace || [])].filter(Boolean).join("\n");

  const handleCopy = () => {
    navigator.clipboard.writeText(fullText);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div
      className={cn("border border-destructive/20 bg-destructive/5", className)}
    >
      {/* Error message header */}
      <div className="p-4 flex items-start gap-3">
        <div className="flex-1 min-w-0">
          <p className="font-medium text-destructive break-words">
            {error || "Unknown error"}
          </p>
        </div>
        <div className="flex items-center gap-1 shrink-0">
          <Button
            variant="ghost"
            size="icon"
            className="h-7 w-7"
            onClick={handleCopy}
          >
            {copied ? (
              <Check className="h-3.5 w-3.5 text-success" />
            ) : (
              <Copy className="h-3.5 w-3.5" />
            )}
          </Button>
          {stacktrace && stacktrace.length > 0 && (
            <Button
              variant="ghost"
              size="icon"
              className="h-7 w-7"
              onClick={() => setExpanded(!expanded)}
            >
              <ChevronDown
                className={cn(
                  "h-3.5 w-3.5 transition-transform",
                  expanded && "rotate-180",
                )}
              />
            </Button>
          )}
        </div>
      </div>

      {/* Stack trace */}
      {expanded && stacktrace && stacktrace.length > 0 && (
        <div className="border-t border-destructive/20 p-4">
          <pre className="font-mono text-xs text-muted-foreground overflow-auto max-h-64 whitespace-pre-wrap">
            {stacktrace.map((line, i) => (
              <div
                key={i.toString()}
                className="hover:bg-destructive/10 px-1 -mx-1"
              >
                {formatStackLine(line)}
              </div>
            ))}
          </pre>
        </div>
      )}
    </div>
  );
}

function formatStackLine(line: string): React.ReactNode {
  // Highlight file paths and line numbers
  const fileMatch = line.match(/at (.+?) \((.+?):(\d+):(\d+)\)/);
  if (fileMatch) {
    const [, fnName, filePath, lineNum, colNum] = fileMatch;
    return (
      <>
        <span className="text-muted-foreground">at </span>
        <span className="text-foreground">{fnName}</span>
        <span className="text-muted-foreground"> (</span>
        <span className="text-primary">{filePath}</span>
        <span className="text-muted-foreground">:</span>
        <span className="text-warning">{lineNum}</span>
        <span className="text-muted-foreground">:</span>
        <span className="text-warning">{colNum}</span>
        <span className="text-muted-foreground">)</span>
      </>
    );
  }

  // Simple file path format
  const simpleMatch = line.match(/at (.+?):(\d+):(\d+)/);
  if (simpleMatch) {
    const [, filePath, lineNum, colNum] = simpleMatch;
    return (
      <>
        <span className="text-muted-foreground">at </span>
        <span className="text-primary">{filePath}</span>
        <span className="text-muted-foreground">:</span>
        <span className="text-warning">{lineNum}</span>
        <span className="text-muted-foreground">:</span>
        <span className="text-warning">{colNum}</span>
      </>
    );
  }

  return line;
}