import { useMemo, type ReactNode } from 'react' import { motion } from 'framer-motion' import { cn } from '@/lib/utils' import { Highlight } from 'prism-react-renderer' import { CollapsibleCard } from '../basic/collapsible-card' import { FileLine, LoaderLine } from '../basic/icons-inline' import { getFileIconColorClass } from '../lib/file-icon-maps' const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0' /** Prism 主题:使用设计令牌,随 data-theme 生效 */ const codeBlockTheme = { plain: { color: 'var(--color-text)' }, styles: [ { types: ['comment', 'prolog', 'cdata'], style: { color: 'var(--color-text-tertiary)' } }, { types: ['keyword', 'atrule'], style: { color: 'var(--color-blue)' } }, { types: ['string', 'attr-value', 'inserted'], style: { color: 'var(--color-success)' } }, { types: ['number', 'constant', 'boolean'], style: { color: 'var(--color-success)' } }, { types: ['function', 'function-variable'], style: { color: 'var(--color-purple)' } }, { types: ['punctuation', 'operator', 'deleted'], style: { color: 'var(--color-text-tertiary)' } }, { types: ['property', 'tag', 'selector'], style: { color: 'var(--color-pink)' } }, { types: ['regex', 'important', 'variable'], style: { color: 'var(--color-text)' } }, ], } const LANG_EXT: Record = { javascript: 'js', typescript: 'ts', jsx: 'jsx', tsx: 'tsx', python: 'py', java: 'java', go: 'go', rust: 'rs', html: 'html', css: 'css', json: 'json', yaml: 'yml', markdown: 'md', shell: 'sh', bash: 'sh', } export type CodeBlockDiffType = 'add' | 'remove' export interface CodeBlockPartProps { language?: string code?: string filename?: string diffType?: CodeBlockDiffType | null diffLines?: number | null isGenerating?: boolean defaultExpanded?: boolean className?: string headerIcon?: ReactNode getFileIcon?: (fileType?: string, filename?: string) => ReactNode } function getDisplayFilename(filename?: string, language?: string): string { if (filename && filename.trim()) return filename const lang = (language ?? 'text').toLowerCase() const ext = LANG_EXT[lang] ?? 'txt' return `${lang}.${ext}` } export function CodeBlockPart({ language = 'javascript', code = '', filename, diffType = null, diffLines = null, isGenerating = false, defaultExpanded = true, className, headerIcon, getFileIcon, }: CodeBlockPartProps) { const displayName = useMemo( () => getDisplayFilename(filename, language), [filename, language] ) const calculatedDiffLines = useMemo(() => { if (diffType && code) { const lines = code.split('\n') return lines.length } return diffLines ?? null }, [code, diffType, diffLines]) const fileType = language?.toLowerCase() const defaultHeaderIcon = isGenerating ? ( ) : getFileIcon ? ( getFileIcon(fileType, displayName) ) : ( ) const headerRight = diffType && calculatedDiffLines != null ? ( {diffType === 'add' ? '+' : '-'}{calculatedDiffLines} ) : null const contentClass = cn( 'overflow-x-auto text-left scrollbar-auto', diffType === 'add' && 'bg-success-bg border-l-[3px] border-l-[var(--color-success-border)] -ml-5 pl-5', diffType === 'remove' && 'bg-error-bg border-l-[3px] border-l-[var(--color-error-border)] -ml-5 pl-5' ) const lang = (language ?? 'text').toLowerCase() return ( {displayName}} headerRight={headerRight} defaultExpanded={defaultExpanded} contentPadding="var(--spacing-2) 0 var(--spacing-2) var(--spacing-5)" titleShimmer={isGenerating} className={className} >
{({ className: preClassName, style, tokens, getLineProps, getTokenProps }) => (
              
                {tokens.map((line, i) => (
                  
{line.map((token, k) => ( ))}
))}
)}
) } CodeBlockPart.displayName = 'CodeBlockPart' export const CodeBlockCard = CodeBlockPart export type CodeBlockCardProps = CodeBlockPartProps