File size: 3,215 Bytes
9b72f0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useState } from "react";
import showdown from "showdown";

const converter = new showdown.Converter();
interface ParsedContent {
  thinkContent: string | null;
  afterContent: string;
  isThinking: boolean;
}

function parseThinkTags(content: string): ParsedContent {
  const openTagIndex = content.indexOf("<think>");

  if (openTagIndex === -1) {
    return {
      thinkContent: null,
      afterContent: content,
      isThinking: false,
    };
  }

  const closeTagIndex = content.indexOf("</think>");

  if (closeTagIndex === -1) {
    return {
      thinkContent: content.slice(openTagIndex + 7),
      afterContent: "",
      isThinking: true,
    };
  }

  return {
    thinkContent: content.slice(openTagIndex + 7, closeTagIndex),
    afterContent: content.slice(closeTagIndex + 8),
    isThinking: false,
  };
}

export default function MessageContent({ content }: { content: string }) {
  const [showThinking, setShowThinking] = useState(false);
  const [thinkingTime, setThinkingTime] = useState(0);
  const parsed = parseThinkTags(content);

  useEffect(() => {
    if (parsed.isThinking) {
      const startTime = Date.now();
      const interval = setInterval(() => {
        setThinkingTime((Date.now() - startTime) / 1000);
      }, 100);
      return () => clearInterval(interval);
    }
  }, [parsed.isThinking]);

  if (!parsed.thinkContent) {
    return (
      <div
        className="prose prose-sm dark:prose-invert prose-headings:font-semibold prose-headings:mt-4 prose-headings:mb-2 prose-h3:text-base prose-p:my-2 prose-ul:my-2 prose-li:my-0 max-w-none"
        dangerouslySetInnerHTML={{
          __html: converter.makeHtml(content),
        }}
      />
    );
  }

  return (
    <div className="space-y-2">
      {parsed.isThinking ? (
        <div className="flex items-center gap-2 text-gray-600 dark:text-gray-400">
          <div className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
          <span className="text-xs">
            Thinking for {thinkingTime.toFixed(1)}s...
          </span>
        </div>
      ) : (
        <div>
          <button
            onClick={() => setShowThinking(!showThinking)}
            className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
          >
            {showThinking ? "Hide" : "Show"} thinking
          </button>
          {showThinking && (
            <div
              className="prose dark:prose-invert prose-li:text-xs prose-headings:text-xs prose-p:text-xs prose-headings:font-semibold prose-p:my-2 prose-ul:my-2 prose-li:my-0 prose-hr:my-4 max-w-none"
              dangerouslySetInnerHTML={{
                __html: converter.makeHtml(parsed.thinkContent),
              }}
            />
          )}
        </div>
      )}
      {parsed.afterContent && (
        <div
          className="prose dark:prose-invert prose-li:text-sm prose-headings:text-sm prose-p:text-sm prose-headings:font-semibold prose-p:my-2 prose-ul:my-2 prose-li:my-0 prose-hr:my-4 max-w-none"
          dangerouslySetInnerHTML={{
            __html: converter.makeHtml(parsed.afterContent),
          }}
        />
      )}
    </div>
  );
}