/** * CodeSnippetPanel - 代码片段快速浏览面板 * * 功能: * - 搜索过滤代码片段 * - 按语言筛选 * - 展开/折叠查看完整代码 * - 一键复制 * - 下载为文件 */ import { useState, useMemo, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Search, Copy, Download, ChevronDown, ChevronUp, FileCode, X, Check, Code } from 'lucide-react'; import { Button, EmptyState } from './ui'; import type { CodeBlock } from '../store/chatStore'; // === Types === export interface CodeSnippet { id: string; block: CodeBlock; messageIndex: number; } interface CodeSnippetPanelProps { snippets: CodeSnippet[]; } // === Language Colors === const LANGUAGE_COLORS: Record = { python: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', javascript: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300', typescript: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', rust: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300', go: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300', java: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300', cpp: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300', c: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', html: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300', css: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300', sql: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300', bash: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', shell: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', json: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', yaml: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', markdown: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', text: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400', }; function getLanguageColor(lang: string): string { return LANGUAGE_COLORS[lang.toLowerCase()] || LANGUAGE_COLORS.text; } function getFileExtension(lang?: string): string { const extensions: Record = { python: 'py', javascript: 'js', typescript: 'ts', rust: 'rs', go: 'go', java: 'java', cpp: 'cpp', c: 'c', html: 'html', css: 'css', sql: 'sql', bash: 'sh', shell: 'sh', json: 'json', yaml: 'yaml', markdown: 'md', }; return extensions[lang?.toLowerCase() || ''] || 'txt'; } // === Snippet Card Component === interface SnippetCardProps { snippet: CodeSnippet; isExpanded: boolean; onToggle: () => void; } function SnippetCard({ snippet, isExpanded, onToggle }: SnippetCardProps) { const [copied, setCopied] = useState(false); const { block, messageIndex } = snippet; const handleCopy = useCallback(async (e: React.MouseEvent) => { e.stopPropagation(); try { await navigator.clipboard.writeText(block.content || ''); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy:', err); } }, [block.content]); const handleDownload = useCallback((e: React.MouseEvent) => { e.stopPropagation(); const filename = block.filename || `snippet_${messageIndex + 1}.${getFileExtension(block.language)}`; const blob = new Blob([block.content || ''], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }, [block.content, block.filename, block.language, messageIndex]); const lineCount = (block.content || '').split('\n').length; const charCount = (block.content || '').length; const previewLines = (block.content || '').split('\n').slice(0, 2).join('\n'); return (
{/* Header */} {isExpanded ? ( ) : ( )}
{/* Preview / Full Content */} {isExpanded ? (
                {block.content}
              
) : (
              {previewLines}
              {(block.content || '').split('\n').length > 2 && '...'}
            
)}
); } // === Main Component === export function CodeSnippetPanel({ snippets }: CodeSnippetPanelProps) { const [searchQuery, setSearchQuery] = useState(''); const [selectedLanguage, setSelectedLanguage] = useState(null); const [expandedId, setExpandedId] = useState(null); // Get unique languages with counts const languages = useMemo(() => { const langMap = new Map(); snippets.forEach(s => { const lang = s.block.language?.toLowerCase() || 'text'; langMap.set(lang, (langMap.get(lang) || 0) + 1); }); return Array.from(langMap.entries()) .map(([lang, count]) => ({ lang, count })) .sort((a, b) => b.count - a.count); }, [snippets]); // Filter snippets const filteredSnippets = useMemo(() => { return snippets.filter(snippet => { const block = snippet.block; // Language filter if (selectedLanguage && (block.language?.toLowerCase() || 'text') !== selectedLanguage) { return false; } // Search filter if (searchQuery) { const query = searchQuery.toLowerCase(); const matchesFilename = block.filename?.toLowerCase().includes(query); const matchesContent = block.content?.toLowerCase().includes(query); const matchesLanguage = block.language?.toLowerCase().includes(query); return matchesFilename || matchesContent || matchesLanguage; } return true; }); }, [snippets, searchQuery, selectedLanguage]); const handleToggle = useCallback((id: string) => { setExpandedId(prev => prev === id ? null : id); }, []); if (snippets.length === 0) { return ( } title="暂无代码片段" description="对话中生成的代码会自动出现在这里" className="py-8" /> ); } return (
{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-8 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent" /> {searchQuery && ( )}
{/* Language Filters */} {languages.length > 1 && (
{languages.map(({ lang, count }) => ( ))}
)} {/* Results Count */} {(searchQuery || selectedLanguage) && (
找到 {filteredSnippets.length} 个代码片段
)} {/* Snippet List */}
{filteredSnippets.map((snippet) => ( handleToggle(snippet.id)} /> ))} {filteredSnippets.length === 0 && (
没有找到匹配的代码片段
)}
); }