| 'use client'; | |
| import { useState } from 'react'; | |
| import { useRouter } from 'next/navigation'; | |
| export default function KnowledgeBuilder() { | |
| const router = useRouter(); | |
| const [currentStep, setCurrentStep] = useState(0); | |
| const [knowledgeData, setKnowledgeData] = useState({ | |
| name: '', | |
| description: '', | |
| category: 'general', | |
| isPublic: false, | |
| tags: [], | |
| files: [], | |
| webSources: [], | |
| manualContent: '', | |
| processingSettings: { | |
| autoChunk: true, | |
| smartDedup: true, | |
| generateSummary: false, | |
| chunkSize: 1000, | |
| overlap: 200 | |
| } | |
| }); | |
| const [uploadedFiles, setUploadedFiles] = useState([]); | |
| const [webSources, setWebSources] = useState([]); | |
| const [newUrl, setNewUrl] = useState(''); | |
| const [activeTab, setActiveTab] = useState('files'); | |
| const steps = [ | |
| { id: 0, name: '基本信息', icon: 'ri-information-line' }, | |
| { id: 1, name: '内容添加', icon: 'ri-file-add-line' }, | |
| { id: 2, name: '处理配置', icon: 'ri-settings-3-line' }, | |
| { id: 3, name: '预览测试', icon: 'ri-eye-line' } | |
| ]; | |
| const categories = [ | |
| { key: 'general', label: '通用知识', icon: 'ri-book-line' }, | |
| { key: 'tech', label: '技术文档', icon: 'ri-code-line' }, | |
| { key: 'product', label: '产品说明', icon: 'ri-product-hunt-line' }, | |
| { key: 'service', label: '客服问答', icon: 'ri-customer-service-line' }, | |
| { key: 'legal', label: '法务合规', icon: 'ri-scales-line' }, | |
| { key: 'sales', label: '销售材料', icon: 'ri-line-chart-line' } | |
| ]; | |
| const handleNext = () => { | |
| if (currentStep < steps.length - 1) { | |
| setCurrentStep(currentStep + 1); | |
| } | |
| }; | |
| const handlePrev = () => { | |
| if (currentStep > 0) { | |
| setCurrentStep(currentStep - 1); | |
| } | |
| }; | |
| const handleSave = () => { | |
| console.log('保存知识库:', knowledgeData); | |
| router.push('/knowledge'); | |
| }; | |
| const handlePublish = () => { | |
| console.log('发布知识库:', knowledgeData); | |
| router.push('/knowledge'); | |
| }; | |
| const handleFileUpload = (event) => { | |
| 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 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 renderStepContent = () => { | |
| switch (currentStep) { | |
| case 0: | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <div className="space-y-6"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">知识库名称</label> | |
| <input | |
| type="text" | |
| placeholder="输入知识库名称..." | |
| value={knowledgeData.name} | |
| onChange={(e) => setKnowledgeData({...knowledgeData, name: 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" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">描述说明</label> | |
| <textarea | |
| placeholder="描述知识库的用途和内容范围..." | |
| rows={4} | |
| maxLength={500} | |
| value={knowledgeData.description} | |
| onChange={(e) => setKnowledgeData({...knowledgeData, description: 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 resize-none" | |
| /> | |
| <div className="text-xs text-gray-500 mt-1">{knowledgeData.description.length}/500 字符</div> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-3">知识库分类</label> | |
| <div className="grid grid-cols-2 gap-3"> | |
| {categories.map((category) => ( | |
| <div | |
| key={category.key} | |
| onClick={() => setKnowledgeData({...knowledgeData, category: category.key})} | |
| className={`p-4 border rounded-lg cursor-pointer transition-all ${ | |
| knowledgeData.category === category.key | |
| ? 'border-blue-500 bg-blue-50' | |
| : 'border-gray-200 hover:border-gray-300' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-3"> | |
| <div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"> | |
| <i className={`${category.icon} text-blue-600`}></i> | |
| </div> | |
| <span className="font-medium text-gray-900">{category.label}</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-6"> | |
| <div className="bg-blue-50 border border-blue-200 rounded-lg p-6"> | |
| <div className="flex items-start space-x-3"> | |
| <div className="w-6 h-6 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 className="border border-gray-200 rounded-lg p-6"> | |
| <h4 className="font-medium text-gray-900 mb-4">权限设置</h4> | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <div className="font-medium text-gray-900">公开知识库</div> | |
| <div className="text-sm text-gray-500">其他用户可以查看和使用此知识库</div> | |
| </div> | |
| <div | |
| onClick={() => setKnowledgeData({...knowledgeData, isPublic: !knowledgeData.isPublic})} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| knowledgeData.isPublic ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| knowledgeData.isPublic ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 1: | |
| return ( | |
| <div className="space-y-6"> | |
| <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-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4"> | |
| <i className="ri-upload-cloud-line text-2xl text-gray-400"></i> | |
| </div> | |
| <p className="text-lg text-gray-600 mb-2">拖拽文件到此处,或者</p> | |
| <label className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700 transition-colors"> | |
| 选择文件 | |
| <input | |
| type="file" | |
| multiple | |
| accept=".pdf,.doc,.docx,.txt,.md,.csv,.xlsx" | |
| onChange={handleFileUpload} | |
| className="hidden" | |
| /> | |
| </label> | |
| <p className="text-sm text-gray-500 mt-4">支持 PDF、DOC、DOCX、TXT、MD、CSV、XLSX 格式,单文件最大 50MB</p> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'web' && ( | |
| <div className="space-y-6"> | |
| <div className="flex space-x-3"> | |
| <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" | |
| /> | |
| <button | |
| onClick={addWebSource} | |
| className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap" | |
| > | |
| 开始抓取 | |
| </button> | |
| </div> | |
| <div className="text-sm text-gray-500 space-y-1"> | |
| <p>• 支持抓取文档站点、博客文章、产品页面等</p> | |
| <p>• 自动识别并提取页面文本内容</p> | |
| <p>• 支持批量抓取同站点多个页面</p> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'manual' && ( | |
| <div className="space-y-4"> | |
| <textarea | |
| placeholder="直接输入要添加到知识库的内容... 您可以添加: • 产品使用说明和操作指南 • 常见问题及详细解答 • 专业术语和概念解释 • 业务流程和规范文档" | |
| rows={12} | |
| maxLength={500} | |
| value={knowledgeData.manualContent} | |
| onChange={(e) => setKnowledgeData({...knowledgeData, manualContent: 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 resize-none" | |
| /> | |
| <div className="flex items-center justify-between"> | |
| <div className="text-sm text-gray-500">{knowledgeData.manualContent.length}/500 字符</div> | |
| <button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap"> | |
| 添加内容 | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 2: | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <div className="bg-white border border-gray-200 rounded-lg p-6"> | |
| <h4 className="font-medium text-gray-900 mb-4">处理选项</h4> | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <span className="font-medium text-gray-900">自动分块处理</span> | |
| <p className="text-sm text-gray-500">将长文档自动分割为合适大小的片段</p> | |
| </div> | |
| <div | |
| onClick={() => setKnowledgeData(prev => ({ | |
| ...prev, | |
| processingSettings: {...prev.processingSettings, autoChunk: !prev.processingSettings.autoChunk} | |
| }))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| knowledgeData.processingSettings.autoChunk ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| knowledgeData.processingSettings.autoChunk ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <span className="font-medium text-gray-900">智能去重</span> | |
| <p className="text-sm text-gray-500">自动识别并合并重复或相似内容</p> | |
| </div> | |
| <div | |
| onClick={() => setKnowledgeData(prev => ({ | |
| ...prev, | |
| processingSettings: {...prev.processingSettings, smartDedup: !prev.processingSettings.smartDedup} | |
| }))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| knowledgeData.processingSettings.smartDedup ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| knowledgeData.processingSettings.smartDedup ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <span className="font-medium text-gray-900">生成摘要</span> | |
| <p className="text-sm text-gray-500">为每个内容片段生成简要摘要</p> | |
| </div> | |
| <div | |
| onClick={() => setKnowledgeData(prev => ({ | |
| ...prev, | |
| processingSettings: {...prev.processingSettings, generateSummary: !prev.processingSettings.generateSummary} | |
| }))} | |
| className={`w-10 h-6 rounded-full relative cursor-pointer transition-colors ${ | |
| knowledgeData.processingSettings.generateSummary ? 'bg-blue-600' : 'bg-gray-300' | |
| }`} | |
| > | |
| <div className={`w-4 h-4 bg-white rounded-full absolute top-1 transition-transform ${ | |
| knowledgeData.processingSettings.generateSummary ? 'right-1' : 'left-1' | |
| }`}></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white border border-gray-200 rounded-lg p-6"> | |
| <h4 className="font-medium text-gray-900 mb-4">参数设置</h4> | |
| <div className="space-y-6"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| 分块大小: {knowledgeData.processingSettings.chunkSize} 字符 | |
| </label> | |
| <input | |
| type="range" | |
| min="500" | |
| max="2000" | |
| step="100" | |
| value={knowledgeData.processingSettings.chunkSize} | |
| onChange={(e) => setKnowledgeData(prev => ({ | |
| ...prev, | |
| processingSettings: {...prev.processingSettings, chunkSize: parseInt(e.target.value)} | |
| }))} | |
| className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" | |
| /> | |
| <div className="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>500</span> | |
| <span>2000</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| 重叠字符: {knowledgeData.processingSettings.overlap} | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="500" | |
| step="50" | |
| value={knowledgeData.processingSettings.overlap} | |
| onChange={(e) => setKnowledgeData(prev => ({ | |
| ...prev, | |
| processingSettings: {...prev.processingSettings, overlap: parseInt(e.target.value)} | |
| }))} | |
| className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" | |
| /> | |
| <div className="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>0</span> | |
| <span>500</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 3: | |
| return ( | |
| <div className="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 lg:grid-cols-2 gap-6"> | |
| <div className="space-y-4"> | |
| <div className="p-4 bg-gray-50 rounded-lg"> | |
| <div className="text-sm text-gray-600 mb-2">基本信息</div> | |
| <div className="space-y-2"> | |
| <div><span className="font-medium">名称:</span>{knowledgeData.name || '未设置'}</div> | |
| <div><span className="font-medium">分类:</span>{categories.find(c => c.key === knowledgeData.category)?.label}</div> | |
| <div><span className="font-medium">描述:</span>{knowledgeData.description || '无描述'}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div className="p-4 bg-gray-50 rounded-lg"> | |
| <div className="text-sm text-gray-600 mb-2">内容统计</div> | |
| <div className="space-y-2"> | |
| <div><span className="font-medium">文档数量:</span>{uploadedFiles.length} 个</div> | |
| <div><span className="font-medium">网页来源:</span>{webSources.length} 个</div> | |
| <div><span className="font-medium">总分块数:</span>{uploadedFiles.reduce((sum, file) => sum + file.chunks, 0)} 个</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| return ( | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-200"> | |
| {/* 步骤导航 */} | |
| <div className="p-6 border-b border-gray-200"> | |
| <div className="flex items-center justify-between"> | |
| {steps.map((step, index) => ( | |
| <div key={step.id} className="flex items-center"> | |
| <div className={`flex items-center space-x-3 px-4 py-2 rounded-lg cursor-pointer transition-colors ${ | |
| currentStep === index | |
| ? 'bg-blue-100 text-blue-700' | |
| : currentStep > index | |
| ? 'bg-green-100 text-green-700' | |
| : 'text-gray-500' | |
| }`} onClick={() => setCurrentStep(index)}> | |
| <div className="w-6 h-6 flex items-center justify-center"> | |
| <i className={step.icon}></i> | |
| </div> | |
| <span className="font-medium">{step.name}</span> | |
| </div> | |
| {index < steps.length - 1 && ( | |
| <div className="mx-4 w-8 h-px bg-gray-300"></div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* 步骤内容 */} | |
| <div className="p-8"> | |
| {renderStepContent()} | |
| </div> | |
| {/* 底部操作按钮 */} | |
| <div className="p-6 border-t border-gray-200 flex items-center justify-between"> | |
| <div className="flex space-x-3"> | |
| {currentStep > 0 && ( | |
| <button | |
| onClick={handlePrev} | |
| className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap" | |
| > | |
| 上一步 | |
| </button> | |
| )} | |
| </div> | |
| <div className="flex space-x-3"> | |
| <button | |
| onClick={handleSave} | |
| className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors cursor-pointer whitespace-nowrap" | |
| > | |
| 保存草稿 | |
| </button> | |
| {currentStep < steps.length - 1 ? ( | |
| <button | |
| onClick={handleNext} | |
| className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer whitespace-nowrap" | |
| > | |
| 下一步 | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={handlePublish} | |
| className="bg-green-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-green-700 transition-colors cursor-pointer whitespace-nowrap" | |
| > | |
| <div className="w-5 h-5 flex items-center justify-center inline-block mr-2"> | |
| <i className="ri-check-line"></i> | |
| </div> | |
| 创建知识库 | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |