| 'use client'; | |
| import { useState } from 'react'; | |
| interface KnowledgeBaseProps { | |
| data: any; | |
| onChange: (data: any) => void; | |
| } | |
| export default function KnowledgeBase({ data, onChange }: KnowledgeBaseProps) { | |
| const [uploadedFiles, setUploadedFiles] = useState([ | |
| { id: 1, name: '产品手册.pdf', size: '2.4 MB', status: 'uploaded', type: 'pdf', chunks: 45, lastModified: '2024-01-15' }, | |
| { id: 2, name: '常见问题FAQ.docx', size: '856 KB', status: 'uploaded', type: 'docx', chunks: 23, lastModified: '2024-01-14' }, | |
| { id: 3, name: '技术文档.md', size: '1.2 MB', status: 'uploaded', type: 'md', chunks: 67, lastModified: '2024-01-13' }, | |
| ]); | |
| const [webSources, setWebSources] = useState([ | |
| { id: 1, url: 'https://docs.example.com', title: '官方文档', status: 'crawled', pages: 15, lastUpdate: '2024-01-15' }, | |
| { id: 2, url: 'https://help.example.com', title: '帮助中心', status: 'crawling', pages: 8, lastUpdate: '2024-01-14' } | |
| ]); | |
| const [manualContent, setManualContent] = useState(''); | |
| const [newUrl, setNewUrl] = useState(''); | |
| const [activeTab, setActiveTab] = useState('files'); | |
| const [processingSettings, setProcessingSettings] = useState({ | |
| autoChunk: true, | |
| smartDedup: true, | |
| generateSummary: false, | |
| chunkSize: 1000, | |
| overlap: 200 | |
| }); | |
| const knowledgeBases = [ | |
| { id: 'kb-001', name: '通用知识库', description: '包含基础常识和通用信息', items: 1240, isDefault: true }, | |
| { id: 'kb-002', name: '技术文档库', description: '技术相关的文档和资料', items: 567, isDefault: false }, | |
| { id: 'kb-003', name: '产品说明库', description: '产品相关的说明和手册', items: 892, isDefault: false }, | |
| { id: 'kb-004', name: '客服问答库', description: '客服常见问题和解答', items: 345, isDefault: false } | |
| ]; | |
| const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState(['kb-001']); | |
| const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const files = Array.from(event.target.files || []); | |
| const newFiles = files.map((file, index) => ({ | |
| id: uploadedFiles.length + index + 1, | |
| name: file.name, | |
| size: `${(file.size / 1024 / 1024).toFixed(1)} MB`, | |
| status: 'uploading', | |
| type: file.name.split('.').pop() || 'unknown', | |
| chunks: 0, | |
| lastModified: new Date().toISOString().split('T')[0] | |
| })); | |
| setUploadedFiles([...uploadedFiles, ...newFiles]); | |
| setTimeout(() => { | |
| setUploadedFiles(prev => prev.map(file => | |
| newFiles.some(nf => nf.id === file.id) | |
| ? { ...file, status: 'uploaded', chunks: Math.floor(Math.random() * 50) + 10 } | |
| : file | |
| )); | |
| }, 2000); | |
| }; | |
| const removeFile = (fileId: number) => { | |
| setUploadedFiles(prev => prev.filter(file => file.id !== fileId)); | |
| }; | |
| const addWebSource = () => { | |
| if (newUrl) { | |
| const newSource = { | |
| id: webSources.length + 1, | |
| url: newUrl, | |
| title: '正在获取标题...', | |
| status: 'crawling', | |
| pages: 0, | |
| lastUpdate: new Date().toISOString().split('T')[0] | |
| }; | |
| setWebSources([...webSources, newSource]); | |
| setNewUrl(''); | |
| setTimeout(() => { | |
| setWebSources(prev => prev.map(source => | |
| source.id === newSource.id | |
| ? { ...source, status: 'crawled', title: `${newUrl.split('/')[2]} - 文档`, pages: Math.floor(Math.random() * 20) + 5 } | |
| : source | |
| )); | |
| }, 3000); | |
| } | |
| }; | |
| const removeWebSource = (sourceId: number) => { | |
| setWebSources(prev => prev.filter(source => source.id !== sourceId)); | |
| }; | |
| const toggleKnowledgeBase = (kbId: string) => { | |
| setSelectedKnowledgeBases(prev => | |
| prev.includes(kbId) | |
| ? prev.filter(id => id !== kbId) | |
| : [...prev, kbId] | |
| ); | |
| }; | |
| const addManualContent = () => { | |
| if (manualContent.trim()) { | |
| const newFile = { | |
| id: uploadedFiles.length + 1, | |
| name: `手动内容_${new Date().toLocaleTimeString()}`, | |
| size: `${Math.ceil(manualContent.length / 1024)} KB`, | |
| status: 'uploaded', | |
| type: 'text', | |
| chunks: Math.ceil(manualContent.length / processingSettings.chunkSize), | |
| lastModified: new Date().toISOString().split('T')[0] | |
| }; | |
| setUploadedFiles([...uploadedFiles, newFile]); | |
| setManualContent(''); | |
| } | |
| }; | |
| return ( | |
| <div className="space-y-8"> | |
| <div> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-2">知识库配置</h3> | |
| <p className="text-gray-600 mb-6">为Agent配置知识来源,提升回答的准确性和专业性</p> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <div className="lg:col-span-2 space-y-6"> | |
| {/* 选择现有知识库 */} | |
| <div className="bg-white border border-gray-200 rounded-lg p-6"> | |
| <h4 className="font-medium text-gray-900 mb-4">选择现有知识库</h4> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| {knowledgeBases.map((kb) => ( | |
| <div | |
| key={kb.id} | |
| onClick={() => toggleKnowledgeBase(kb.id)} | |
| className={`p-4 border rounded-lg cursor-pointer transition-all ${ | |
| selectedKnowledgeBases.includes(kb.id) | |
| ? 'border-blue-500 bg-blue-50' | |
| : 'border-gray-200 hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2"> | |
| <h5 className="font-medium text-gray-900">{kb.name}</h5> | |
| {kb.isDefault && ( | |
| <span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">默认</span> | |
| )} | |
| </div> | |
| <p className="text-sm text-gray-600 mt-1">{kb.description}</p> | |
| <p className="text-xs text-gray-500 mt-2">{kb.items.toLocaleString()} 条记录</p> | |
| </div> | |
| <div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${ | |
| selectedKnowledgeBases.includes(kb.id) | |
| ? 'border-blue-500 bg-blue-500' | |
| : 'border-gray-300' | |
| }`}> | |
| {selectedKnowledgeBases.includes(kb.id) && ( | |
| <i className="ri-check-line text-white text-xs"></i> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* 添加新内容 */} | |
| <div className="bg-white border border-gray-200 rounded-lg"> | |
| <div className="border-b border-gray-200"> | |
| <div className="flex space-x-0"> | |
| {[ | |
| { key: 'files', label: '文件上传', icon: 'ri-file-line' }, | |
| { key: 'web', label: '网页抓取', icon: 'ri-global-line' }, | |
| { key: 'manual', label: '手动输入', icon: 'ri-edit-line' } | |
| ].map((tab) => ( | |
| <button | |
| key={tab.key} | |
| onClick={() => setActiveTab(tab.key)} | |
| className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${ | |
| activeTab === tab.key | |
| ? 'border-blue-500 text-blue-600 bg-blue-50' | |
| : 'border-transparent text-gray-500 hover:text-gray-700' | |
| }`} | |
| > | |
| <div className="w-4 h-4 flex items-center justify-center inline-block mr-2"> | |
| <i className={tab.icon}></i> | |
| </div> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="p-6"> | |
| {activeTab === 'files' && ( | |
| <div className="space-y-4"> | |
| <div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors"> | |
| <div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4"> | |
| <i className="ri-upload-cloud-line text-xl text-gray-400"></i> | |
| </div> | |
| <p className="text-gray-600 mb-2">拖拽文件到此处,或者</p> | |
| <label className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700 transition-colors text-sm"> | |
| 选择文件 | |
| <input | |
| type="file" | |
| multiple | |
| accept=".pdf,.doc,.docx,.txt,.md,.csv,.xlsx" | |
| onChange={handleFileUpload} | |
| className="hidden" | |
| /> | |
| </label> | |
| <p className="text-xs text-gray-500 mt-2">支持 PDF, DOC, DOCX, TXT, MD, CSV, XLSX 格式,最大 50MB</p> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'web' && ( | |
| <div className="space-y-4"> | |
| <div className="flex space-x-2"> | |
| <input | |
| type="url" | |
| placeholder="输入要抓取的网址(如:https://docs.example.com)" | |
| value={newUrl} | |
| onChange={(e) => setNewUrl(e.target.value)} | |
| className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" | |
| /> | |
| <button | |
| onClick={addWebSource} | |
| className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap text-sm" | |
| > | |
| 开始抓取 | |
| </button> | |
| </div> | |
| <div className="text-xs text-gray-500"> | |
| 支持抓取文档站点、博客文章、产品页面等,自动提取文本内容 | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'manual' && ( | |
| <div className="space-y-4"> | |
| <textarea | |
| placeholder="直接输入要添加到知识库的内容... 例如: • 产品使用说明 • 常见问题解答 • 操作步骤指南 • 专业知识条目" | |
| rows={8} | |
| maxLength={500} | |
| value={manualContent} | |
| onChange={(e) => setManualContent(e.target.value)} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none" | |
| /> | |
| <div className="flex items-center justify-between"> | |
| <div className="text-xs text-gray-500">{manualContent.length}/500 字符</div> | |
| <button | |
| onClick={addManualContent} | |
| disabled={!manualContent.trim()} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors cursor-pointer whitespace-nowrap text-sm" | |
| > | |
| 添加内容 | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-6"> | |
| {/* 已添加内容统计 */} | |
| <div className="bg-white border border-gray-200 rounded-lg p-4"> | |
| <h4 className="font-medium text-gray-900 mb-3">内容统计</h4> | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-600">文档数量</span> | |
| <span className="font-medium text-gray-900">{uploadedFiles.length}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-600">网页来源</span> | |
| <span className="font-medium text-gray-900">{webSources.length}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-600">总分块数</span> | |
| <span className="font-medium text-gray-900">{uploadedFiles.reduce((sum, file) => sum + file.chunks, 0)}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-600">知识库</span> | |
| <span className="font-medium text-gray-900">{selectedKnowledgeBases.length} 个</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 处理设置 */} | |
| <div className="bg-white border border-gray-200 rounded-lg p-4"> | |
| <h4 className="font-medium text-gray-900 mb-3">处理设置</h4> | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-700">自动分块处理</span> | |
| <div | |
| onClick={() => setProcessingSettings(prev => ({...prev, autoChunk: !prev.autoChunk}))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| processingSettings.autoChunk ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| processingSettings.autoChunk ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-700">智能去重</span> | |
| <div | |
| onClick={() => setProcessingSettings(prev => ({...prev, smartDedup: !prev.smartDedup}))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| processingSettings.smartDedup ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| processingSettings.smartDedup ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-gray-700">生成摘要</span> | |
| <div | |
| onClick={() => setProcessingSettings(prev => ({...prev, generateSummary: !prev.generateSummary}))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| processingSettings.generateSummary ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| processingSettings.generateSummary ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| <div className="pt-2 border-t border-gray-200"> | |
| <div className="mb-3"> | |
| <label className="block text-sm text-gray-700 mb-1"> | |
| 分块大小: {processingSettings.chunkSize} 字符 | |
| </label> | |
| <input | |
| type="range" | |
| min="500" | |
| max="2000" | |
| step="100" | |
| value={processingSettings.chunkSize} | |
| onChange={(e) => setProcessingSettings(prev => ({...prev, chunkSize: parseInt(e.target.value)}))} | |
| className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm text-gray-700 mb-1"> | |
| 重叠字符: {processingSettings.overlap} | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="500" | |
| step="50" | |
| value={processingSettings.overlap} | |
| onChange={(e) => setProcessingSettings(prev => ({...prev, overlap: parseInt(e.target.value)}))} | |
| className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 使用提示 */} | |
| <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> | |
| <div className="flex items-start space-x-3"> | |
| <div className="w-5 h-5 flex items-center justify-center mt-0.5"> | |
| <i className="ri-lightbulb-line text-blue-600"></i> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-blue-900 mb-2">优化建议</h4> | |
| <ul className="text-sm text-blue-800 space-y-1"> | |
| <li>• 上传结构化的文档获得更好效果</li> | |
| <li>• 定期更新知识库内容</li> | |
| <li>• 避免重复和冗余信息</li> | |
| <li>• 使用标准的文档格式</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* 已上传文件列表 */} | |
| {uploadedFiles.length > 0 && ( | |
| <div className="mt-8"> | |
| <h4 className="font-medium text-gray-900 mb-4">已添加的文件</h4> | |
| <div className="bg-white border border-gray-200 rounded-lg overflow-hidden"> | |
| <div className="divide-y divide-gray-200"> | |
| {uploadedFiles.map((file) => ( | |
| <div key={file.id} className="p-4 hover:bg-gray-50 transition-colors"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-4"> | |
| <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center"> | |
| <i className={`${ | |
| file.type === 'pdf' ? 'ri-file-pdf-line text-red-600' : | |
| file.type === 'docx' ? 'ri-file-word-line text-blue-600' : | |
| file.type === 'md' ? 'ri-markdown-line text-purple-600' : | |
| 'ri-file-text-line text-gray-600' | |
| }`}></i> | |
| </div> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2"> | |
| <p className="font-medium text-gray-900">{file.name}</p> | |
| {file.status === 'uploading' ? ( | |
| <span className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">处理中</span> | |
| ) : ( | |
| <span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">已完成</span> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-4 mt-1 text-sm text-gray-500"> | |
| <span>{file.size}</span> | |
| <span>{file.chunks} 个分块</span> | |
| <span>修改于 {file.lastModified}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| {file.status === 'uploading' ? ( | |
| <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div> | |
| ) : ( | |
| <button className="p-1 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer"> | |
| <div className="w-4 h-4 flex items-center justify-center"> | |
| <i className="ri-eye-line"></i> | |
| </div> | |
| </button> | |
| )} | |
| <button | |
| onClick={() => removeFile(file.id)} | |
| className="p-1 text-gray-400 hover:text-red-600 transition-colors cursor-pointer" | |
| > | |
| <div className="w-4 h-4 flex items-center justify-center"> | |
| <i className="ri-delete-bin-line"></i> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* 网页来源列表 */} | |
| {webSources.length > 0 && ( | |
| <div className="mt-8"> | |
| <h4 className="font-medium text-gray-900 mb-4">网页来源</h4> | |
| <div className="bg-white border border-gray-200 rounded-lg overflow-hidden"> | |
| <div className="divide-y divide-gray-200"> | |
| {webSources.map((source) => ( | |
| <div key={source.id} className="p-4 hover:bg-gray-50 transition-colors"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-4"> | |
| <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center"> | |
| <i className="ri-global-line text-green-600"></i> | |
| </div> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2"> | |
| <p className="font-medium text-gray-900">{source.title}</p> | |
| {source.status === 'crawling' ? ( | |
| <span className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full">抓取中</span> | |
| ) : ( | |
| <span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">已完成</span> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-4 mt-1 text-sm text-gray-500"> | |
| <span>{source.url}</span> | |
| <span>{source.pages} 个页面</span> | |
| <span>更新于 {source.lastUpdate}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| {source.status === 'crawling' ? ( | |
| <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div> | |
| ) : ( | |
| <button className="p-1 text-gray-400 hover:text-blue-600 transition-colors cursor-pointer"> | |
| <div className="w-4 h-4 flex items-center justify-center"> | |
| <i className="ri-refresh-line"></i> | |
| </div> | |
| </button> | |
| )} | |
| <button | |
| onClick={() => removeWebSource(source.id)} | |
| className="p-1 text-gray-400 hover:text-red-600 transition-colors cursor-pointer" | |
| > | |
| <div className="w-4 h-4 flex items-center justify-center"> | |
| <i className="ri-delete-bin-line"></i> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |