test1 / app /knowledge /create /KnowledgeBuilder.tsx
daios007's picture
init
9eb1c55
'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="直接输入要添加到知识库的内容...&#10;&#10;您可以添加:&#10;• 产品使用说明和操作指南&#10;• 常见问题及详细解答&#10;• 专业术语和概念解释&#10;• 业务流程和规范文档"
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>
);
}