File size: 3,289 Bytes
a414867
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"use client";

import { useState } from "react";
import { NotationHoverCard } from "@/components/tree-visualizer/NotationHoverCard";
import { MathOnlyRenderer } from "@/components/tree-visualizer/RichMath";
import { cn } from "@/lib/utils";
import { splitTokenBlocks } from "@/lib/tree/content";

interface MathRendererProps {
  content: string;
  className?: string;
  compact?: boolean;
  forceTokenOpen?: boolean;
  allowTokenToggle?: boolean;
}

function stopNodeInteraction(event: React.SyntheticEvent) {
  event.stopPropagation();
}

function tokenLabel(name: string) {
  return name.replace(/[^a-zA-Z0-9_]+/g, "_");
}

function TokenBlob({
  name,
  value,
  raw,
  forceOpen,
  allowToggle = true
}: {
  name: string;
  value: string;
  raw: string;
  forceOpen?: boolean;
  allowToggle?: boolean;
}) {
  const [open, setOpen] = useState(false);
  const isFinalAnswer = name === "final_answer";
  const isOpen = Boolean(forceOpen || open);

  return (
    <div className="my-2 block min-w-0">
      <NotationHoverCard label={tokenLabel(name)} rendered={value} notation={raw}>
        <button
          type="button"
          onPointerDown={stopNodeInteraction}
          onMouseDown={stopNodeInteraction}
          onClick={(event) => {
            stopNodeInteraction(event);
            if (!forceOpen && allowToggle) setOpen((current) => !current);
          }}
          className={cn(
            "token-blob inline-flex items-center gap-2 rounded-full border px-3 py-1 text-[11px] font-semibold shadow-sm transition",
            isFinalAnswer
              ? "border-claude-300 bg-claude-50 text-claude-700 hover:bg-claude-100"
              : "border-paper-300 bg-white text-ink-600 hover:bg-paper-50"
          )}
          title={allowToggle || forceOpen ? (isOpen ? `Hide ${tokenLabel(name)}` : `Show ${tokenLabel(name)}`) : tokenLabel(name)}
        >
          <span className="h-1.5 w-1.5 rounded-full bg-current opacity-70" />
          {tokenLabel(name)}
        </button>
      </NotationHoverCard>
      {isOpen && (
        <div className="token-content mt-2 block min-w-0 rounded-lg border border-paper-200 bg-white/86 px-3 py-2 text-sm leading-6 text-ink-700 shadow-sm">
          <MathOnlyRenderer content={value} />
        </div>
      )}
    </div>
  );
}

export function MathRenderer({
  content,
  className,
  compact = false,
  forceTokenOpen = false,
  allowTokenToggle = true
}: MathRendererProps) {
  const segments = splitTokenBlocks(content || "");

  if (!content.trim()) {
    return <p className={cn("text-sm text-ink-300", className)}>No math content.</p>;
  }

  return (
    <div
      className={cn(
        "whitespace-pre-wrap text-sm leading-6 text-ink-700",
        compact && "line-clamp-4",
        className
      )}
    >
      {segments.map((segment, index) => {
        if (segment.type === "token") {
          return (
            <TokenBlob
              key={`${segment.name}-${index}`}
              name={segment.name}
              value={segment.value}
              raw={segment.raw}
              forceOpen={forceTokenOpen}
              allowToggle={allowTokenToggle}
            />
          );
        }
        return <MathOnlyRenderer key={`text-${index}`} content={segment.value} />;
      })}
    </div>
  );
}