Files
new-api/web/default/src/components/ai-elements/code-block.tsx
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

189 lines
4.6 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
/* eslint-disable react-refresh/only-export-components */
'use client'
import {
type ComponentProps,
createContext,
type HTMLAttributes,
useContext,
useEffect,
useState,
} from 'react'
import type { Element } from 'hast'
import { CheckIcon, CopyIcon } from 'lucide-react'
import {
type BundledLanguage,
codeToHtml,
type ShikiTransformer,
} from 'shiki/bundle/web'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string
language: BundledLanguage
showLineNumbers?: boolean
}
type CodeBlockContextType = {
code: string
}
const CodeBlockContext = createContext<CodeBlockContextType>({
code: '',
})
const lineNumberTransformer: ShikiTransformer = {
name: 'line-numbers',
line(node: Element, line: number) {
node.children.unshift({
type: 'element',
tagName: 'span',
properties: {
className: [
'inline-block',
'min-w-10',
'mr-4',
'text-right',
'select-none',
'text-muted-foreground',
],
},
children: [{ type: 'text', value: String(line) }],
})
},
}
export async function highlightCode(
code: string,
language: BundledLanguage,
showLineNumbers = false
) {
const transformers: ShikiTransformer[] = showLineNumbers
? [lineNumberTransformer]
: []
return codeToHtml(code, {
lang: language,
themes: {
light: 'one-light',
dark: 'one-dark-pro',
},
transformers,
})
}
export const CodeBlock = ({
code,
language,
showLineNumbers = false,
className,
children,
...props
}: CodeBlockProps) => {
const [html, setHtml] = useState<string>('')
useEffect(() => {
let cancelled = false
highlightCode(code, language, showLineNumbers).then((next) => {
if (!cancelled) {
setHtml(next)
}
})
return () => {
cancelled = true
}
}, [code, language, showLineNumbers])
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
'group bg-background text-foreground relative w-full overflow-hidden rounded-md border',
className
)}
{...props}
>
<div className='relative'>
<div
className='[&>pre]:bg-background! [&>pre]:text-foreground! overflow-hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:p-4 [&>pre]:text-sm'
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: html }}
/>
{children && (
<div className='absolute top-2 right-2 flex items-center gap-2'>
{children}
</div>
)}
</div>
</div>
</CodeBlockContext.Provider>
)
}
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void
onError?: (error: Error) => void
timeout?: number
}
export const CodeBlockCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false)
const { code } = useContext(CodeBlockContext)
const copyToClipboard = async () => {
if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
onError?.(new Error('Clipboard API not available'))
return
}
try {
await navigator.clipboard.writeText(code)
setIsCopied(true)
onCopy?.()
setTimeout(() => setIsCopied(false), timeout)
} catch (error) {
onError?.(error as Error)
}
}
const Icon = isCopied ? CheckIcon : CopyIcon
return (
<Button
className={cn('shrink-0', className)}
onClick={copyToClipboard}
size='icon'
variant='ghost'
{...props}
>
{children ?? <Icon size={14} />}
</Button>
)
}