feat: P3a 代码语法高亮 (highlight.js + CodeBlock组件)

This commit is contained in:
yuzhiran-dev
2026-05-18 12:06:30 +08:00
parent 21078ae287
commit 4de042abcd
6 changed files with 112 additions and 44 deletions
+3
View File
@@ -4,6 +4,9 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
/* highlight.js for code syntax highlighting */
@import 'highlight.js/styles/atom-one-dark.css';
@layer base {
:root {
--background: 0 0% 100%;
+13 -1
View File
@@ -8,6 +8,7 @@ import { getToken, apiFetch } from '@/lib/auth';
import { DEFAULT_MODEL } from '@/lib/models';
import { ModelSelector } from '@/components/ui/model-selector';
import { useT } from '@/i18n';
import { CodeBlock } from '@/components/ui/code-block';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
@@ -412,7 +413,18 @@ function SandboxPage() {
<div className="w-8 h-8 bg-brand-600 rounded-xl flex items-center justify-center text-white text-sm font-bold shrink-0">Y</div>
)}
<div className={`max-w-[75%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${msg.role === 'user' ? 'bg-brand-600 text-white rounded-tr-none' : 'bg-muted text-foreground rounded-tl-none'}`}>
{msg.content}
{(() => {
const parts = msg.content.split(/(```(?:\w+)?\n[\s\S]*?```)/g)
return parts.map((part, pi) => {
const match = part.match(/^```(\w*)\n([\s\S]*)```$/)
if (match) {
const [, lang, code] = match
return <CodeBlock key={pi} code={code.trim()} language={lang || undefined} />
}
if (pi > 0 && parts[pi-1].startsWith('```')) return null
return <span key={pi} className="whitespace-pre-wrap">{part}</span>
})
})()}
</div>
{msg.role === 'user' && (
<div className="w-8 h-8 bg-muted-foreground/20 rounded-xl flex items-center justify-center text-xs font-bold shrink-0"></div>
+68
View File
@@ -0,0 +1,68 @@
'use client'
import { useRef, useState } from 'react'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import python from 'highlight.js/lib/languages/python'
import typescript from 'highlight.js/lib/languages/typescript'
import xml from 'highlight.js/lib/languages/xml'
import css from 'highlight.js/lib/languages/css'
import bash from 'highlight.js/lib/languages/bash'
import json from 'highlight.js/lib/languages/json'
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('js', javascript)
hljs.registerLanguage('python', python)
hljs.registerLanguage('py', python)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('ts', typescript)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('css', css)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('sh', bash)
hljs.registerLanguage('json', json)
interface CodeBlockProps {
code: string
language?: string
}
export function CodeBlock({ code, language }: CodeBlockProps) {
const [copied, setCopied] = useState(false)
const codeRef = useRef<HTMLElement>(null)
let highlighted = ''
try {
if (language && hljs.getLanguage(language)) {
highlighted = hljs.highlight(code, { language }).value
} else {
highlighted = hljs.highlightAuto(code).value
}
} catch {
highlighted = code
}
async function copyCode() {
try {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {}
}
return (
<div className="relative group my-2 rounded-xl overflow-hidden border border-border">
<div className="flex items-center justify-between px-4 py-1.5 bg-muted/80 border-b border-border">
<span className="text-xs text-muted-foreground">{language || 'code'}</span>
<button onClick={copyCode}
className="text-xs px-2 py-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
{copied ? '已复制' : '复制'}
</button>
</div>
<pre className="overflow-x-auto p-4 text-sm leading-relaxed bg-[#1e1e1e]">
<code ref={codeRef} className={`language-${language || ''}`} dangerouslySetInnerHTML={{ __html: highlighted }} />
</pre>
</div>
)
}