Spaces:
Running
Running
File size: 3,727 Bytes
7ac2545 |
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 128 129 130 |
import type React from "react";
import { useState, useRef, useEffect } from "react";
import { Copy, Check } from "lucide-react";
import hljs from "highlight.js";
import "highlight.js/styles/github-dark.css";
interface CodeBlockProps {
language: string;
code: string;
}
const CodeBlock: React.FC<CodeBlockProps> = ({ language, code }) => {
const [copied, setCopied] = useState(false);
const codeRef = useRef<HTMLElement>(null);
useEffect(() => {
if (codeRef.current) {
codeRef.current.removeAttribute("data-highlighted");
hljs.highlightElement(codeRef.current);
}
}, [code, language]);
const handleCopy = async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="rounded-xl overflow-hidden border border-white/10 bg-[#0d1117] my-3">
<div className="flex items-center justify-between px-4 py-2 bg-white/5 border-b border-white/10">
<span className="text-xs font-mono text-gray-400">
{language || "code"}
</span>
<button
onClick={handleCopy}
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-white transition-colors"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? "Copied" : "Copy"}
</button>
</div>
<pre className="p-4 overflow-x-auto text-sm leading-relaxed !bg-[#0d1117]">
<code ref={codeRef} className={language ? `language-${language}` : ""}>
{code}
</code>
</pre>
</div>
);
};
function looksLikeCode(text: string): boolean {
const result = hljs.highlightAuto(text);
return result.relevance > 5;
}
function parseContent(text: string): React.ReactNode[] {
const parts: React.ReactNode[] = [];
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
const textBefore = text.slice(lastIndex, match.index);
parts.push(
<span key={`text-${lastIndex}`} className="whitespace-pre-wrap">
{textBefore}
</span>,
);
}
parts.push(
<CodeBlock
key={`code-${match.index}`}
language={match[1]}
code={match[2].trimEnd()}
/>,
);
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
const remaining = text.slice(lastIndex);
// If no code fences were found and the content looks like code, render as code block
if (lastIndex === 0 && remaining.trim().length > 0 && looksLikeCode(remaining)) {
const detected = hljs.highlightAuto(remaining);
parts.push(
<CodeBlock
key="code-auto"
language={detected.language || ""}
code={remaining.trimEnd()}
/>,
);
} else {
parts.push(
<span key={`text-${lastIndex}`} className="whitespace-pre-wrap">
{remaining}
</span>,
);
}
}
return parts;
}
export const ChatMessage: React.FC<{
role: "user" | "assistant";
content: string;
}> = ({ role, content }) => {
if (role === "user") {
return (
<div className="flex justify-end">
<div className="px-4 py-3 rounded-2xl max-w-lg bg-emerald-600/20 border border-emerald-500/30">
<p className="text-white whitespace-pre-wrap">{content}</p>
</div>
</div>
);
}
return (
<div className="flex justify-start">
<div className="px-4 py-3 rounded-2xl max-w-2xl bg-white/5 border border-white/10">
<div className="text-gray-200">{parseContent(content)}</div>
</div>
</div>
);
};
|