Spaces:
Running
Running
| 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> | |
| ); | |
| }; | |