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>
);
}
|