1599 lines
65 KiB
JavaScript
1599 lines
65 KiB
JavaScript
/**
|
||
* TalkingQ Platform - 创建知识库向导页面
|
||
* 包含3步流程:基础信息 -> 数据选择 -> 索引设置
|
||
*
|
||
* API预留:
|
||
* - POST /api/v1/knowledge-bases - 创建知识库
|
||
* - POST /api/v1/knowledge-bases/validate-name - 验证名称唯一性
|
||
* - POST /api/v1/files/upload - 文件上传
|
||
* - GET /api/v1/files/upload-status/:id - 获取上传状态
|
||
*/
|
||
|
||
registerPage('kb-create', function(container) {
|
||
// ==================== 状态管理 ====================
|
||
let currentStep = 1;
|
||
const totalSteps = 3;
|
||
let hasUnsavedChanges = false;
|
||
const DRAFT_KEY = 'kb_create_draft';
|
||
|
||
// ==================== API预留 - 后端接口定义 ====================
|
||
/**
|
||
* 创建知识库
|
||
* POST /api/v1/knowledge-bases
|
||
* Body: { name, description, type, sourceType, sourceConfig, indexConfig }
|
||
* Response: { id: string, name: string, status: string }
|
||
*/
|
||
const API_CREATE = '/api/v1/knowledge-bases';
|
||
|
||
/**
|
||
* 验证名称唯一性
|
||
* POST /api/v1/knowledge-bases/validate-name
|
||
* Body: { name: string }
|
||
* Response: { valid: boolean, message?: string }
|
||
*/
|
||
const API_VALIDATE_NAME = '/api/v1/knowledge-bases/validate-name';
|
||
|
||
/**
|
||
* 文件上传
|
||
* POST /api/v1/files/upload
|
||
* FormData: { file, category, knowledgeBaseId? }
|
||
* Response: { fileId: string, fileName: string, status: string }
|
||
*/
|
||
const API_UPLOAD = '/api/v1/files/upload';
|
||
|
||
/**
|
||
* 获取上传/解析状态
|
||
* GET /api/v1/files/status/:fileId
|
||
* Response: { status: 'uploading'|'processing'|'ready'|'error', progress?: number, error?: string }
|
||
*/
|
||
const API_FILE_STATUS = '/api/v1/files/status/:fileId';
|
||
|
||
/**
|
||
* 获取类目列表
|
||
* GET /api/v1/categories
|
||
* Response: { list: [{ id, name, fileCount }] }
|
||
*/
|
||
const API_GET_CATEGORIES = '/api/v1/categories';
|
||
|
||
// ==================== 表单数据 ====================
|
||
const formData = {
|
||
// 步骤1: 基础信息
|
||
name: '',
|
||
description: '',
|
||
type: '',
|
||
|
||
// 步骤2: 数据选择
|
||
sourceType: 'upload', // upload | category | files
|
||
category: '',
|
||
uploadedFiles: [],
|
||
autoSync: false,
|
||
parseMethod: 'inherit', // inherit | default | custom
|
||
|
||
// 自定义解析配置
|
||
docParseMethods: ['electronic'],
|
||
imageParseMethods: ['smart'],
|
||
videoParseMethods: ['media'],
|
||
enableVideoParse: false,
|
||
|
||
// 步骤3: 索引设置
|
||
sliceMethod: 'smart', // smart | length | page | title | regex | symbol
|
||
maxChunkLength: 2000,
|
||
overlapLength: 200,
|
||
titleLevel: 3,
|
||
regexPattern: '',
|
||
symbolChars: ',',
|
||
enableMetadataExtract: true,
|
||
enableExcelHeader: false,
|
||
enableMultiTurn: false,
|
||
enableOCR: false,
|
||
enableAutoKeywords: false,
|
||
enableAutoTags: false,
|
||
vectorModel: 'text-embedding-v2',
|
||
rerankModel: 'bge-reranker-v2.5',
|
||
rerankMode: 'qa', // qa | similarity | custom
|
||
vectorTopK: 60,
|
||
keywordTopK: 60,
|
||
similarityThreshold: 0.5,
|
||
maxRecallCount: 10,
|
||
vectorStorage: 'platform'
|
||
};
|
||
|
||
// ==================== 知识库类型定义 ====================
|
||
const kbTypes = [
|
||
{
|
||
id: 'doc_search',
|
||
icon: 'file-text',
|
||
title: '文档搜索',
|
||
desc: '构建文档、文件、图片、Excel混合型索引知识库',
|
||
tooltip: '构建文档、文件、图片、Excel混合型索引知识库,构建常规混合搜索知识库。支持PDF、Word、Excel、图片等多种格式文档。'
|
||
},
|
||
{
|
||
id: 'data_query',
|
||
icon: 'table',
|
||
title: '数据查询',
|
||
desc: '基于NL2SQL的表格数据查询',
|
||
tooltip: '构建仅以数据表结构查询的数据索引体系,基于NL2SQL思路,实现表头、列头等结构化数据查询能力。适合结构化数据问答场景。'
|
||
},
|
||
{
|
||
id: 'image_qa',
|
||
icon: 'image',
|
||
title: '图片问答',
|
||
desc: '基于多模态Embedding的图片检索',
|
||
tooltip: '构建仅以图片索引为主的知识库,基于多模态Embedding能力,实现图片名称与图片内容搜索。适合图片素材检索场景。'
|
||
},
|
||
{
|
||
id: 'video_search',
|
||
icon: 'video',
|
||
title: '音视频搜索',
|
||
desc: '音视频内容智能理解',
|
||
tooltip: '构建音视频搜索知识库,基于高精度解析与多模态Embedding检索技术,对音视频内容进行智能理解与提炼。支持字幕提取、内容摘要等。'
|
||
}
|
||
];
|
||
|
||
// ==================== 切片方式 ====================
|
||
const sliceMethods = [
|
||
{ id: 'smart', label: '智能切分', desc: '根据语义自动识别段落边界' },
|
||
{ id: 'length', label: '按长度切分', desc: '固定长度切分,支持重叠' },
|
||
{ id: 'page', label: '按页切分', desc: '按页分割文档' },
|
||
{ id: 'title', label: '按标题切分', desc: '按标题层级结构切分' },
|
||
{ id: 'regex', label: '按正则切分', desc: '自定义正则表达式切分' },
|
||
{ id: 'symbol', label: '按符号切分', desc: '按标点符号切分' }
|
||
];
|
||
|
||
// ==================== 符号选项 ====================
|
||
const symbolOptions = [
|
||
{ value: ',', label: '中文逗号 ,' },
|
||
{ value: '。', label: '中文句号 。' },
|
||
{ value: ',', label: '中文逗号(英文)' },
|
||
{ value: '.', label: '英文句号 .' },
|
||
{ value: '、', label: '顿号 、' },
|
||
{ value: '?', label: '中文问号 ?' },
|
||
{ value: '!', label: '中文感叹号 !' },
|
||
{ value: '?', label: '英文问号 ?' },
|
||
{ value: '!', label: '英文感叹号 !' },
|
||
{ value: '\n', label: '换行符' },
|
||
{ value: '\n\n', label: '双换行符' }
|
||
];
|
||
|
||
// ==================== 类目数据 ====================
|
||
const categories = [
|
||
{ id: 'cat-001', name: '默认类目', fileCount: 128 },
|
||
{ id: 'cat-002', name: '教学资料', fileCount: 56 },
|
||
{ id: 'cat-003', name: '产品文档', fileCount: 34 },
|
||
{ id: 'cat-004', name: '用户指南', fileCount: 89 },
|
||
];
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return str.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function getFormatsByType() {
|
||
switch (formData.type) {
|
||
case 'image_qa':
|
||
return '.png,.jpg,.jpeg,.bmp,.gif';
|
||
case 'video_search':
|
||
return '.mp4,.avi,.mov,.mkv,.wmv,.mp3,.wav,.m4a';
|
||
case 'data_query':
|
||
return '.xlsx,.xls,.csv';
|
||
default:
|
||
return '.pdf,.doc,.docx,.txt,.md,.ppt,.pptx,.xlsx,.xls,.png,.jpg,.jpeg,.gif';
|
||
}
|
||
}
|
||
|
||
function getFileIcon(filename) {
|
||
const ext = filename?.split('.').pop()?.toLowerCase() || '';
|
||
const icons = {
|
||
pdf: '📄', doc: '📄', docx: '📄', txt: '📝', md: '📋',
|
||
xlsx: '📊', xls: '📊', csv: '📊',
|
||
png: '🖼', jpg: '🖼', jpeg: '🖼', gif: '🖼', bmp: '🖼',
|
||
mp4: '🎬', avi: '🎬', mov: '🎬', mkv: '🎬', wmv: '🎬',
|
||
mp3: '🎵', wav: '🎵', m4a: '🎵'
|
||
};
|
||
return icons[ext] || '📁';
|
||
}
|
||
|
||
function getFileStatusText(status) {
|
||
const map = {
|
||
pending: '待上传',
|
||
uploading: '上传中',
|
||
processing: '解析中',
|
||
ready: '解析完成',
|
||
error: '解析失败'
|
||
};
|
||
return map[status] || status;
|
||
}
|
||
|
||
// ==================== 草稿管理 ====================
|
||
|
||
function loadDraft() {
|
||
try {
|
||
const draft = localStorage.getItem(DRAFT_KEY);
|
||
if (draft) {
|
||
const saved = JSON.parse(draft);
|
||
Object.assign(formData, saved.formData || {});
|
||
currentStep = saved.currentStep || 1;
|
||
return true;
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load draft:', e);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function saveDraft() {
|
||
try {
|
||
localStorage.setItem(DRAFT_KEY, JSON.stringify({
|
||
formData,
|
||
currentStep,
|
||
savedAt: new Date().toISOString()
|
||
}));
|
||
Toast.success('草稿已保存');
|
||
} catch (e) {
|
||
Toast.warning('草稿保存失败');
|
||
}
|
||
}
|
||
|
||
function clearDraft() {
|
||
localStorage.removeItem(DRAFT_KEY);
|
||
}
|
||
|
||
// ==================== 验证函数 ====================
|
||
|
||
function validateStep1() {
|
||
const errors = [];
|
||
|
||
if (!formData.name.trim()) {
|
||
errors.push('请输入知识库名称');
|
||
} else if (formData.name.length < 2 || formData.name.length > 64) {
|
||
errors.push('知识库名称长度需在2-64字符之间');
|
||
}
|
||
|
||
if (!formData.type) {
|
||
errors.push('请选择知识库类型');
|
||
}
|
||
|
||
return errors;
|
||
}
|
||
|
||
function validateStep2() {
|
||
const errors = [];
|
||
|
||
if (formData.sourceType === 'upload' && formData.uploadedFiles.length === 0) {
|
||
errors.push('请上传至少一个文件');
|
||
}
|
||
|
||
if (formData.sourceType === 'upload') {
|
||
const failedFiles = formData.uploadedFiles.filter(f => f.status === 'error');
|
||
if (failedFiles.length > 0) {
|
||
errors.push(`有 ${failedFiles.length} 个文件上传失败,请处理后重试`);
|
||
}
|
||
}
|
||
|
||
return errors;
|
||
}
|
||
|
||
function validateStep3() {
|
||
return []; // 步骤3一般不需要强制验证
|
||
}
|
||
|
||
// ==================== 渲染函数 ====================
|
||
|
||
/**
|
||
* 渲染向导步骤
|
||
*/
|
||
function renderWizardSteps() {
|
||
const steps = ['基础信息', '数据选择', '索引设置'];
|
||
return `
|
||
<div class="wizard-steps">
|
||
${[1, 2, 3].map((step, idx) => `
|
||
<div class="wizard-step ${step < currentStep ? 'completed' : ''} ${step === currentStep ? 'active' : ''}">
|
||
<div class="wizard-step-num">
|
||
${step < currentStep ? '<i data-lucide="check" style="width:14px;height:14px"></i>' : step}
|
||
</div>
|
||
<span class="wizard-step-label">${steps[step - 1]}</span>
|
||
</div>
|
||
${idx < 2 ? `<div class="wizard-step-line ${step < currentStep ? 'completed' : ''}"></div>` : ''}
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染步骤1:基础信息
|
||
*/
|
||
function renderStep1() {
|
||
return `
|
||
<div class="wizard-content">
|
||
<h3 style="font-size:16px;font-weight:600;margin-bottom:24px">
|
||
基础知识信息
|
||
<span class="help-tip" onclick="window.__showTooltip('step1Tip')">?</span>
|
||
</h3>
|
||
|
||
<div id="step1Tip" class="tooltip-box" style="display:none;margin-bottom:20px">
|
||
<div style="padding:16px;font-size:13px;color:#fff;line-height:1.6">
|
||
<p><strong>知识库名称</strong>:用于标识知识库,建议简洁明了</p>
|
||
<p style="margin-top:8px"><strong>知识库类型</strong>:根据您的数据类型选择合适的类型</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 知识库名称 -->
|
||
<div class="form-group" style="margin-bottom:20px">
|
||
<label class="form-label">
|
||
知识库名称 <span style="color:var(--danger)">*</span>
|
||
</label>
|
||
<input type="text" class="form-control" id="kbName"
|
||
placeholder="请输入知识库名称,2-64个字符"
|
||
maxlength="64"
|
||
value="${escapeHtml(formData.name)}"
|
||
oninput="window.__updateCharCount(this,64);hasUnsavedChanges=true">
|
||
<div class="char-counter" id="kbNameCounter">${formData.name.length}/64</div>
|
||
</div>
|
||
|
||
<!-- 知识库描述 -->
|
||
<div class="form-group" style="margin-bottom:24px">
|
||
<label class="form-label">
|
||
知识库描述
|
||
<span class="help-tip" onclick="window.__showTooltip('descTip')">?</span>
|
||
</label>
|
||
<div id="descTip" class="tooltip-box" style="display:none;margin-bottom:8px">
|
||
<div style="padding:12px;font-size:13px;color:#fff">
|
||
描述可以帮助您和其他用户理解知识库的内容和用途。可选填写。
|
||
</div>
|
||
</div>
|
||
<textarea class="form-control" id="kbDesc" placeholder="请输入知识库描述(可选)" rows="3" maxlength="500"
|
||
oninput="window.__updateCharCount(this,500);hasUnsavedChanges=true">${escapeHtml(formData.description)}</textarea>
|
||
<div class="char-counter" id="kbDescCounter">${formData.description.length}/500</div>
|
||
</div>
|
||
|
||
<!-- 知识库类型 -->
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
知识库类型 <span style="color:var(--danger)">*</span>
|
||
</label>
|
||
<div class="kb-type-grid">
|
||
${kbTypes.map(type => `
|
||
<div class="kb-type-card ${formData.type === type.id ? 'selected' : ''}"
|
||
onclick="window.__selectKbType('${type.id}')" data-type="${type.id}">
|
||
<div class="check-icon"><i data-lucide="check" style="width:12px;height:12px"></i></div>
|
||
<div class="kb-type-card-icon"><i data-lucide="${type.icon}" style="width:32px;height:32px"></i></div>
|
||
<div class="kb-type-card-title">${type.title}</div>
|
||
<div class="kb-type-card-desc">${type.desc}</div>
|
||
<div class="kb-type-tooltip" style="display:none;position:absolute;bottom:-60px;left:50%;transform:translateX(-50%);background:#1a1a1a;color:#fff;padding:8px 12px;border-radius:6px;font-size:12px;white-space:nowrap;z-index:10;box-shadow:0 4px 12px rgba(0,0,0,0.2)">
|
||
${type.tooltip}
|
||
<div style="position:absolute;top:-6px;left:50%;transform:translateX(-50%);border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #1a1a1a"></div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-help-link" onclick="window.__showKbTypeHelp()">
|
||
<i data-lucide="help-circle" style="width:14px;height:14px"></i>
|
||
了解各类型区别
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染步骤2:数据选择
|
||
*/
|
||
function renderStep2() {
|
||
const formats = getFormatsByType();
|
||
const maxFiles = 50;
|
||
const maxSize = formData.type === 'image_qa' || formData.type === 'video_search' ? '20MB' : '50MB';
|
||
|
||
return `
|
||
<div class="wizard-content">
|
||
<h3 style="font-size:16px;font-weight:600;margin-bottom:24px">选择数据源</h3>
|
||
|
||
<!-- 数据连接器说明 -->
|
||
<div class="api-note" style="margin-bottom:20px">
|
||
<i data-lucide="info" style="width:16px;height:16px;flex-shrink:0"></i>
|
||
<div>
|
||
<strong>默认文件连接器</strong>
|
||
<p style="margin-top:4px;font-size:12px">从连接器内选择已解析文档构建知识索引,也支持直接上传文件解析并构建索引,上传文件将自动存储至连接器。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据来源类型 -->
|
||
<div class="data-source-tabs">
|
||
<div class="data-source-tab ${formData.sourceType === 'upload' ? 'active' : ''}"
|
||
onclick="window.__selectSourceType('upload')">
|
||
<i data-lucide="upload" style="width:14px;height:14px;margin-right:6px;vertical-align:middle"></i>
|
||
文件上传
|
||
</div>
|
||
<div class="data-source-tab ${formData.sourceType === 'category' ? 'active' : ''}"
|
||
onclick="window.__selectSourceType('category')">
|
||
<i data-lucide="folder" style="width:14px;height:14px;margin-right:6px;vertical-align:middle"></i>
|
||
选择类目
|
||
</div>
|
||
<div class="data-source-tab ${formData.sourceType === 'files' ? 'active' : ''}"
|
||
onclick="window.__selectSourceType('files')">
|
||
<i data-lucide="file" style="width:14px;height:14px;margin-right:6px;vertical-align:middle"></i>
|
||
选择文件
|
||
</div>
|
||
</div>
|
||
|
||
<div id="sourceContent">
|
||
${renderSourceContent(formats, maxFiles, maxSize)}
|
||
</div>
|
||
|
||
<!-- 自动同步开关 -->
|
||
${formData.sourceType !== 'upload' ? `
|
||
<div class="sync-switch" style="margin-top:20px">
|
||
<div class="sync-switch-label">
|
||
<div>
|
||
开启自动同步知识索引
|
||
<span class="help-tip" onclick="window.__showTooltip('autoSyncTip')">?</span>
|
||
</div>
|
||
<div id="autoSyncTip" class="tooltip-box" style="display:none;margin-top:8px">
|
||
<div style="padding:12px;font-size:13px;color:#fff">
|
||
开启自动同步后,当所选类目下的文件发生新增、修改、删除时,系统会自动更新知识库索引,无需手动操作。
|
||
</div>
|
||
</div>
|
||
<div class="sync-switch-desc">
|
||
开启后将自动同步类目下所有文件,在类目文档发生变化后,自动重建及更新索引。
|
||
</div>
|
||
</div>
|
||
<label class="switch">
|
||
<input type="checkbox" id="autoSync" ${formData.autoSync ? 'checked' : ''} onchange="window.__toggleAutoSync(this.checked)">
|
||
<span class="switch-slider"></span>
|
||
</label>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染数据源内容
|
||
*/
|
||
function renderSourceContent(formats, maxFiles, maxSize) {
|
||
switch (formData.sourceType) {
|
||
case 'upload':
|
||
return renderUploadContent(formats, maxFiles, maxSize);
|
||
case 'category':
|
||
return renderCategoryContent();
|
||
case 'files':
|
||
return renderFilesContent();
|
||
default:
|
||
return renderUploadContent(formats, maxFiles, maxSize);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染文件上传内容
|
||
*/
|
||
function renderUploadContent(formats, maxFiles, maxSize) {
|
||
return `
|
||
<!-- 类目选择 -->
|
||
<div class="category-selector">
|
||
<span style="font-size:13px;font-weight:500;flex-shrink:0">配置类目:</span>
|
||
<select class="form-control" id="uploadCategory" style="flex:1;max-width:300px"
|
||
onchange="window.__updateCategory(this.value)">
|
||
<option value="">默认类目</option>
|
||
${categories.map(c => `<option value="${c.id}" ${formData.category === c.id ? 'selected' : ''}>${c.name} (${c.fileCount}个文件)</option>`).join('')}
|
||
</select>
|
||
<button class="btn btn-outline btn-sm" onclick="window.__showCategoryDrawer()">
|
||
<i data-lucide="plus" style="width:14px;height:14px"></i> 新建类目
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 上传区域 -->
|
||
<div class="upload-zone" id="uploadZone"
|
||
ondragover="window.__handleDragOver(event)" ondragleave="window.__handleDragLeave(event)" ondrop="window.__handleDrop(event)">
|
||
<div class="upload-zone-icon">📁</div>
|
||
<div class="upload-zone-text">点击或拖拽文件到此处上传</div>
|
||
<div style="display:flex;gap:12px;justify-content:center;align-items:center">
|
||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('fileInput').click()">
|
||
<i data-lucide="folder-open" style="width:14px;height:14px"></i> 选择文件
|
||
</button>
|
||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('folderInput').click()">
|
||
<i data-lucide="folder" style="width:14px;height:14px"></i> 选择文件夹
|
||
</button>
|
||
</div>
|
||
<input type="file" id="fileInput" multiple style="display:none" accept="${formats}"
|
||
onchange="window.__handleFileSelect(this.files)">
|
||
<input type="file" id="folderInput" webkitdirectory multiple style="display:none"
|
||
onchange="window.__handleFileSelect(this.files)">
|
||
<div class="upload-zone-hint">
|
||
支持格式: ${formats}<br>
|
||
单文件最大: ${maxSize} | 最多: ${maxFiles}个文件
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件列表 -->
|
||
${formData.uploadedFiles.length > 0 ? `
|
||
<div class="file-list">
|
||
<div class="file-list-header">
|
||
<span style="flex:1">文件名</span>
|
||
<span style="width:80px">大小</span>
|
||
<span style="width:120px">状态</span>
|
||
<span style="width:80px">操作</span>
|
||
</div>
|
||
${formData.uploadedFiles.map((file, idx) => `
|
||
<div class="file-list-item">
|
||
<div class="file-icon">${getFileIcon(file.name)}</div>
|
||
<div class="file-info">
|
||
<div class="file-name">${escapeHtml(file.name)}</div>
|
||
<div class="file-meta">${file.size}</div>
|
||
</div>
|
||
<span class="file-status ${file.status}">
|
||
${file.status === 'uploading' ? `<span class="spinner"></span>` : ''}
|
||
${file.status === 'processing' ? `<span class="spinner"></span>` : ''}
|
||
${getFileStatusText(file.status)}
|
||
</span>
|
||
<div class="file-actions">
|
||
${file.status === 'error' ? `
|
||
<button class="btn btn-ghost btn-sm" data-tooltip="重试上传" onclick="window.__retryUpload(${idx})">
|
||
<i data-lucide="refresh-cw" style="width:14px;height:14px"></i>
|
||
</button>
|
||
` : ''}
|
||
<button class="btn btn-ghost btn-sm" data-tooltip="删除文件" onclick="window.__removeFile(${idx})">
|
||
<i data-lucide="x" style="width:14px;height:14px"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div style="margin-top:12px;font-size:12px;color:var(--text-muted);text-align:right">
|
||
已选择 ${formData.uploadedFiles.length}/${maxFiles} 个文件
|
||
<button class="btn btn-ghost btn-sm" style="margin-left:12px" onclick="window.__clearAllFiles()">清空</button>
|
||
</div>
|
||
` : ''}
|
||
|
||
<!-- 解析方式 -->
|
||
<div style="margin-top:24px">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<span style="font-size:13px;font-weight:500">解析方式</span>
|
||
<span class="help-tip" onclick="window.__showTooltip('parseMethodTip')">?</span>
|
||
</div>
|
||
<div id="parseMethodTip" class="tooltip-box" style="display:none;margin-bottom:8px">
|
||
<div style="padding:12px;font-size:13px;color:#fff">
|
||
<p><strong>继承目录配置</strong>:使用类目中预设的解析配置</p>
|
||
<p style="margin-top:8px"><strong>默认解析</strong>:使用系统默认的文档解析方式</p>
|
||
<p style="margin-top:8px"><strong>自定义解析</strong>:根据文档类型选择不同的解析器</p>
|
||
</div>
|
||
</div>
|
||
<div class="parse-method-selector">
|
||
<div class="parse-method-option ${formData.parseMethod === 'inherit' ? 'selected' : ''}" onclick="window.__selectParseMethod('inherit')">
|
||
<i data-lucide="settings" style="width:14px;height:14px"></i>
|
||
继承目录配置
|
||
</div>
|
||
<div class="parse-method-option ${formData.parseMethod === 'default' ? 'selected' : ''}" onclick="window.__selectParseMethod('default')">
|
||
<i data-lucide="check-circle" style="width:14px;height:14px"></i>
|
||
默认解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.parseMethod === 'custom' ? 'selected' : ''}" onclick="window.__selectParseMethod('custom')">
|
||
<i data-lucide="edit" style="width:14px;height:14px"></i>
|
||
自定义解析
|
||
</div>
|
||
</div>
|
||
|
||
${formData.parseMethod === 'custom' ? `
|
||
<div id="parseMethodDetail" style="margin-top:16px;padding:16px;background:var(--bg-base);border-radius:var(--radius)">
|
||
${renderParseMethodDetail()}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染自定义解析方式
|
||
*/
|
||
function renderParseMethodDetail() {
|
||
const type = formData.type;
|
||
let html = '<div style="font-size:13px"><strong>自定义解析方式:</strong></div>';
|
||
|
||
if (['doc_search', 'data_query', ''].includes(type)) {
|
||
html += `
|
||
<div style="margin-top:12px">
|
||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">文档类型</div>
|
||
<div class="parse-method-selector">
|
||
<div class="parse-method-option ${formData.docParseMethods.includes('electronic') ? 'selected' : ''}" onclick="window.__toggleParseMethod('doc','electronic')">
|
||
<i data-lucide="${formData.docParseMethods.includes('electronic') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
电子文档解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.docParseMethods.includes('md') ? 'selected' : ''}" onclick="window.__toggleParseMethod('doc','md')">
|
||
<i data-lucide="${formData.docParseMethods.includes('md') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
Markdown解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.docParseMethods.includes('html') ? 'selected' : ''}" onclick="window.__toggleParseMethod('doc','html')">
|
||
<i data-lucide="${formData.docParseMethods.includes('html') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
HTML解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.docParseMethods.includes('ppt') ? 'selected' : ''}" onclick="window.__toggleParseMethod('doc','ppt')">
|
||
<i data-lucide="${formData.docParseMethods.includes('ppt') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
PPT文档智能解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.docParseMethods.includes('llm') ? 'selected' : ''}" onclick="window.__toggleParseMethod('doc','llm')">
|
||
<i data-lucide="${formData.docParseMethods.includes('llm') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
大模型文档解析
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (type === 'image_qa' || !type) {
|
||
html += `
|
||
<div style="margin-top:16px">
|
||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">图片类型</div>
|
||
<div class="parse-method-selector">
|
||
<div class="parse-method-option ${formData.imageParseMethods.includes('smart') ? 'selected' : ''}" onclick="window.__toggleParseMethod('image','smart')">
|
||
<i data-lucide="${formData.imageParseMethods.includes('smart') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
文档智能解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.imageParseMethods.includes('llm') ? 'selected' : ''}" onclick="window.__toggleParseMethod('image','llm')">
|
||
<i data-lucide="${formData.imageParseMethods.includes('llm') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
大模型文档解析
|
||
</div>
|
||
<div class="parse-method-option ${formData.imageParseMethods.includes('qwen_vl') ? 'selected' : ''}" onclick="window.__toggleParseMethod('image','qwen_vl')">
|
||
<i data-lucide="${formData.imageParseMethods.includes('qwen_vl') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
Qwen VL解析
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (type === 'video_search' || !type) {
|
||
html += `
|
||
<div style="margin-top:16px">
|
||
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">音视频类型</div>
|
||
<div class="parse-method-selector">
|
||
<div class="parse-method-option ${formData.videoParseMethods.includes('media') ? 'selected' : ''}" onclick="window.__toggleParseMethod('video','media')">
|
||
<i data-lucide="${formData.videoParseMethods.includes('media') ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
音视频解析
|
||
</div>
|
||
<label style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px;padding:8px 0;cursor:pointer">
|
||
<input type="checkbox" id="enableVideoParse" ${formData.enableVideoParse ? 'checked' : ''} onchange="formData.enableVideoParse=this.checked">
|
||
开启视频解析
|
||
</label>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* 渲染类目选择内容
|
||
*/
|
||
function renderCategoryContent() {
|
||
return `
|
||
<div class="category-selector">
|
||
<span style="font-size:13px;font-weight:500;flex-shrink:0">选择类目:</span>
|
||
<select class="form-control" id="selectCategory" style="flex:1;max-width:300px"
|
||
onchange="window.__loadCategoryFiles(this.value)">
|
||
${categories.map(c => `<option value="${c.id}">${c.name} (${c.fileCount}个文件)</option>`).join('')}
|
||
</select>
|
||
<button class="btn btn-outline btn-sm" onclick="window.__showCategoryDrawer()">
|
||
<i data-lucide="plus" style="width:14px;height:14px"></i> 新建类目
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="window.__showCategoryDrawer()">
|
||
<i data-lucide="settings" style="width:14px;height:14px"></i> 类目管理
|
||
</button>
|
||
</div>
|
||
|
||
<div class="api-note" style="margin-top:16px">
|
||
<i data-lucide="folder" style="width:16px;height:16px;flex-shrink:0"></i>
|
||
<div>
|
||
<p>选择类目后,该类目下所有文件将被导入并构建索引。</p>
|
||
<p style="margin-top:4px;font-size:12px">当前类目包含 <strong>128</strong> 个文件,预计解析时间约 5-10 分钟。</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染文件选择内容
|
||
*/
|
||
function renderFilesContent() {
|
||
const appFiles = [
|
||
{ name: '产品说明书.pdf', size: '3.2MB', format: 'PDF' },
|
||
{ name: '用户指南.docx', size: '1.5MB', format: 'DOCX' },
|
||
{ name: '技术规格.xlsx', size: '856KB', format: 'XLSX' },
|
||
{ name: '培训视频.mp4', size: '128MB', format: 'MP4' },
|
||
{ name: '常见问题.md', size: '245KB', format: 'MD' },
|
||
];
|
||
|
||
return `
|
||
<div class="category-selector">
|
||
<span style="font-size:13px;font-weight:500;flex-shrink:0">应用数据类目:</span>
|
||
<select class="form-control" id="appCategory" style="flex:1;max-width:300px">
|
||
${categories.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="file-compare">
|
||
<div class="file-compare-panel">
|
||
<div class="file-compare-header">
|
||
<i data-lucide="database" style="width:14px;height:14px"></i>
|
||
数据中心文件
|
||
</div>
|
||
<div class="file-compare-body">
|
||
${appFiles.map((f, idx) => `
|
||
<div class="file-compare-item" onclick="window.__toggleFileSelect(this, '${f.name}')">
|
||
<input type="checkbox" style="margin-right:8px" id="file_${idx}">
|
||
<div class="file-icon">${getFileIcon(f.name)}</div>
|
||
<div>
|
||
<div style="font-size:13px">${f.name}</div>
|
||
<div style="font-size:11px;color:var(--text-muted)">${f.format} · ${f.size}</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="file-compare-panel">
|
||
<div class="file-compare-header">
|
||
<i data-lucide="check-square" style="width:14px;height:14px;color:var(--primary)"></i>
|
||
已选择 <span id="selectedCount">0</span>/50
|
||
</div>
|
||
<div class="file-compare-body" id="selectedAppFiles">
|
||
<div class="file-compare-empty">
|
||
<i data-lucide="inbox" style="width:32px;height:32px;color:var(--text-muted);margin-bottom:8px"></i>
|
||
<p>从左侧选择文件</p>
|
||
<p style="font-size:11px;margin-top:4px">最多支持50个文件</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染步骤3:索引设置
|
||
*/
|
||
function renderStep3() {
|
||
return `
|
||
<div class="wizard-content">
|
||
<h3 style="font-size:16px;font-weight:600;margin-bottom:24px">
|
||
索引设置
|
||
<span class="help-tip" onclick="window.__showTooltip('step3Tip')">?</span>
|
||
</h3>
|
||
|
||
<div id="step3Tip" class="tooltip-box" style="display:none;margin-bottom:20px">
|
||
<div style="padding:16px;font-size:13px;color:#fff;line-height:1.6">
|
||
<p><strong>切片方式</strong>:决定如何将文档分割成小块进行索引。</p>
|
||
<p style="margin-top:8px"><strong>检索配置</strong>:影响检索效果和性能,建议使用推荐配置。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 切片方式 -->
|
||
<div class="form-group" style="margin-bottom:20px">
|
||
<label class="form-label">
|
||
切片方式 <span style="color:var(--danger)">*</span>
|
||
</label>
|
||
<select class="form-control" id="sliceMethod" onchange="window.__updateSliceConfig();hasUnsavedChanges=true">
|
||
${sliceMethods.map(m => `
|
||
<option value="${m.id}" ${formData.sliceMethod === m.id ? 'selected' : ''}>${m.label} - ${m.desc}</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 切片配置 -->
|
||
<div class="slice-config-panel" id="sliceConfigPanel">
|
||
${renderSliceConfig()}
|
||
</div>
|
||
|
||
<!-- 高级配置折叠面板 -->
|
||
<div class="advanced-config" style="margin-top:24px">
|
||
<div class="advanced-config-header" onclick="this.closest('.advanced-config').classList.toggle('open')">
|
||
<span>
|
||
<i data-lucide="settings-2" style="width:14px;height:14px;margin-right:8px;vertical-align:middle"></i>
|
||
高级参数配置
|
||
</span>
|
||
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
|
||
</div>
|
||
<div class="advanced-config-body">
|
||
<!-- 索引增强能力 -->
|
||
<div style="margin-bottom:20px">
|
||
<label class="form-label" style="margin-bottom:12px">索引增强能力</label>
|
||
<div class="parse-method-selector">
|
||
<div class="parse-method-option ${formData.enableMetadataExtract ? 'selected' : ''}" onclick="window.__toggleEnhance('metadataExtract')">
|
||
<i data-lucide="${formData.enableMetadataExtract ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
Metadata抽取
|
||
<span class="help-tip" onclick="window.__showTooltip('metadataTip');event.stopPropagation()">?</span>
|
||
</div>
|
||
<div class="parse-method-option ${formData.enableExcelHeader ? 'selected' : ''}" onclick="window.__toggleEnhance('excelHeader')">
|
||
<i data-lucide="${formData.enableExcelHeader ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
Excel表头拼装
|
||
</div>
|
||
<div class="parse-method-option ${formData.enableMultiTurn ? 'selected' : ''}" onclick="window.__toggleEnhance('multiTurn')">
|
||
<i data-lucide="${formData.enableMultiTurn ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
多轮对话改写
|
||
</div>
|
||
<div class="parse-method-option ${formData.enableOCR ? 'selected' : ''}" onclick="window.__toggleEnhance('ocr')">
|
||
<i data-lucide="${formData.enableOCR ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
OCR增强
|
||
</div>
|
||
<div class="parse-method-option ${formData.enableAutoKeywords ? 'selected' : ''}" onclick="window.__toggleEnhance('autoKeywords')">
|
||
<i data-lucide="${formData.enableAutoKeywords ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
自动关键词提取
|
||
</div>
|
||
<div class="parse-method-option ${formData.enableAutoTags ? 'selected' : ''}" onclick="window.__toggleEnhance('autoTags')">
|
||
<i data-lucide="${formData.enableAutoTags ? 'check-circle' : 'circle'}" style="width:14px;height:14px"></i>
|
||
自动标签提取
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模型配置 -->
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">向量模型</label>
|
||
<select class="form-control" id="vectorModel" onchange="formData.vectorModel=this.value;hasUnsavedChanges=true">
|
||
<option value="text-embedding-v2" ${formData.vectorModel === 'text-embedding-v2' ? 'selected' : ''}>text-embedding-v2 (推荐)</option>
|
||
<option value="text-embedding-3" ${formData.vectorModel === 'text-embedding-3' ? 'selected' : ''}>text-embedding-3</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">排序模型</label>
|
||
<select class="form-control" id="rerankModel" onchange="formData.rerankModel=this.value;hasUnsavedChanges=true">
|
||
<option value="bge-reranker-v2.5" ${formData.rerankModel === 'bge-reranker-v2.5' ? 'selected' : ''}>bge-reranker-v2.5 (推荐)</option>
|
||
<option value="bge-reranker-base" ${formData.rerankModel === 'bge-reranker-base' ? 'selected' : ''}>bge-reranker-base</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">
|
||
排序模式
|
||
<span class="help-tip" onclick="window.__showTooltip('rerankModeTip')">?</span>
|
||
</label>
|
||
<div id="rerankModeTip" class="tooltip-box" style="display:none;margin-bottom:8px">
|
||
<div style="padding:12px;font-size:13px;color:#fff">
|
||
<p><strong>问答模式</strong>:优先返回适合问答场景的召回结果</p>
|
||
<p style="margin-top:4px"><strong>相似模式</strong>:优先返回语义最相近内容</p>
|
||
<p style="margin-top:4px"><strong>自定义模式</strong>:允许自定义排序策略与召回规则</p>
|
||
</div>
|
||
</div>
|
||
<select class="form-control" id="rerankMode" onchange="formData.rerankMode=this.value;hasUnsavedChanges=true">
|
||
<option value="qa" ${formData.rerankMode === 'qa' ? 'selected' : ''}>问答模式 (推荐)</option>
|
||
<option value="similarity" ${formData.rerankMode === 'similarity' ? 'selected' : ''}>相似模式</option>
|
||
<option value="custom" ${formData.rerankMode === 'custom' ? 'selected' : ''}>自定义高级模式</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 检索参数 -->
|
||
<div style="background:var(--bg-base);border-radius:var(--radius);padding:16px;margin-bottom:16px">
|
||
<div style="font-size:13px;font-weight:500;margin-bottom:16px">检索参数配置</div>
|
||
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">初步向量检索TopK</label>
|
||
<div class="range-slider">
|
||
<input type="range" min="10" max="100" value="${formData.vectorTopK}"
|
||
oninput="window.__updateRangeValue(this,'vectorTopK')">
|
||
<span class="range-slider-value" id="vectorTopKValue">${formData.vectorTopK}</span>
|
||
<span class="range-slider-hint">(10-100)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">初步关键词检索TopK</label>
|
||
<div class="range-slider">
|
||
<input type="range" min="10" max="100" value="${formData.keywordTopK}"
|
||
oninput="window.__updateRangeValue(this,'keywordTopK')">
|
||
<span class="range-slider-value" id="keywordTopKValue">${formData.keywordTopK}</span>
|
||
<span class="range-slider-hint">(10-100)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label class="form-label">相似度阈值</label>
|
||
<div class="range-slider">
|
||
<input type="range" min="1" max="100" value="${formData.similarityThreshold * 100}"
|
||
oninput="window.__updateRangeValue(this,'similarityThreshold',100)">
|
||
<span class="range-slider-value" id="similarityThresholdValue">${formData.similarityThreshold.toFixed(2)}</span>
|
||
<span class="range-slider-hint">(0.01-1)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:0">
|
||
<label class="form-label">最终召回最大数量</label>
|
||
<div class="range-slider">
|
||
<input type="range" min="1" max="20" value="${formData.maxRecallCount}"
|
||
oninput="window.__updateRangeValue(this,'maxRecallCount')">
|
||
<span class="range-slider-value" id="maxRecallCountValue">${formData.maxRecallCount}</span>
|
||
<span class="range-slider-hint">(1-20)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-ghost btn-sm" onclick="window.__resetRetrievalParams()" style="margin-top:8px">
|
||
<i data-lucide="rotate-ccw" style="width:14px;height:14px"></i>
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 向量存储 -->
|
||
<div class="form-group" style="margin-top:20px">
|
||
<label class="form-label">向量存储</label>
|
||
<select class="form-control" id="vectorStorage" onchange="formData.vectorStorage=this.value;hasUnsavedChanges=true">
|
||
<option value="platform" ${formData.vectorStorage === 'platform' ? 'selected' : ''}>平台默认向量存储 (推荐)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="save-draft-hint">
|
||
<i data-lucide="save" style="width:12px;height:12px;vertical-align:middle"></i>
|
||
系统会自动保存草稿,您可以随时中断并继续
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 渲染切片配置
|
||
*/
|
||
function renderSliceConfig() {
|
||
switch (formData.sliceMethod) {
|
||
case 'smart':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">
|
||
最大分段长度
|
||
<span class="help-tip" onclick="window.__showTooltip('maxLengthTip')">?</span>
|
||
</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
<div id="maxLengthTip" class="tooltip-box" style="display:none;margin:8px 0">
|
||
<div style="padding:12px;font-size:13px;color:#fff">智能切分时每个切片的最大字符数。</div>
|
||
</div>
|
||
`;
|
||
case 'length':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">
|
||
分段预估长度
|
||
<span class="help-tip" onclick="window.__showTooltip('chunkLengthTip')">?</span>
|
||
</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
<div id="chunkLengthTip" class="tooltip-box" style="display:none;margin:8px 0">
|
||
<div style="padding:12px;font-size:13px;color:#fff">每个切片的目标字符数。</div>
|
||
</div>
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">
|
||
分段重叠长度
|
||
<span class="help-tip" onclick="window.__showTooltip('overlapTip')">?</span>
|
||
</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="overlapLength"
|
||
value="${formData.overlapLength}" min="0" max="1024"
|
||
onchange="formData.overlapLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (0-1024)</span>
|
||
</div>
|
||
</div>
|
||
<div id="overlapTip" class="tooltip-box" style="display:none;margin:8px 0">
|
||
<div style="padding:12px;font-size:13px;color:#fff">相邻切片之间的重叠字符数,用于保证上下文连贯性。</div>
|
||
</div>
|
||
`;
|
||
case 'page':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">
|
||
最大分段长度
|
||
</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
case 'title':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">最大分段长度</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">标题级数</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="titleLevel"
|
||
value="${formData.titleLevel}" min="1" max="5"
|
||
onchange="formData.titleLevel=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">级 (1-5)</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
case 'regex':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">正则表达式</div>
|
||
<div class="slice-config-control">
|
||
<input type="text" class="form-control" id="regexPattern"
|
||
value="${escapeHtml(formData.regexPattern)}" placeholder="例如: \\n\\n"
|
||
style="width:200px"
|
||
onchange="formData.regexPattern=this.value;hasUnsavedChanges=true">
|
||
</div>
|
||
</div>
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">最大分段长度</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
case 'symbol':
|
||
return `
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">符号选择器</div>
|
||
<div class="slice-config-control">
|
||
<select class="form-control" id="symbolChars" style="width:200px"
|
||
onchange="formData.symbolChars=this.value;hasUnsavedChanges=true">
|
||
${symbolOptions.map(s => `
|
||
<option value="${s.value}" ${formData.symbolChars === s.value ? 'selected' : ''}>${s.label}</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="slice-config-row">
|
||
<div class="slice-config-label">最大分段长度</div>
|
||
<div class="slice-config-control">
|
||
<input type="number" class="form-control" style="width:100px" id="maxChunkLength"
|
||
value="${formData.maxChunkLength}" min="10" max="6000"
|
||
onchange="formData.maxChunkLength=parseInt(this.value);hasUnsavedChanges=true">
|
||
<span style="font-size:13px;color:var(--text-muted)">字符 (10-6000)</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
default:
|
||
return '';
|
||
}
|
||
}
|
||
|
||
// ==================== 渲染页面 ====================
|
||
|
||
function renderPage() {
|
||
container.innerHTML = `
|
||
<div class="kb-create-wizard">
|
||
<!-- 页面头部 -->
|
||
<div class="page-header" style="margin-bottom:32px">
|
||
<div class="kb-list-title">
|
||
<button class="page-back-btn" data-tooltip="返回列表" onclick="window.__confirmLeave()">
|
||
<i data-lucide="arrow-left" style="width:16px;height:16px"></i>
|
||
</button>
|
||
<div>
|
||
<h1 class="page-title">创建知识库</h1>
|
||
<p class="page-desc">通过向导创建新的知识库,配置数据源和索引参数</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn btn-outline" onclick="window.__saveDraft();return false">
|
||
<i data-lucide="save" style="width:14px;height:14px"></i>
|
||
保存草稿
|
||
</button>
|
||
<button class="btn btn-ghost" onclick="window.__confirmLeave()">
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
${renderWizardSteps()}
|
||
|
||
<div id="wizardContent">
|
||
${currentStep === 1 ? renderStep1() : currentStep === 2 ? renderStep2() : renderStep3()}
|
||
</div>
|
||
|
||
<div class="wizard-footer">
|
||
<div class="wizard-footer-left">
|
||
${currentStep > 1 ? `
|
||
<button class="btn btn-outline" onclick="window.__prevStep()">
|
||
<i data-lucide="arrow-left" style="width:14px;height:14px"></i>
|
||
上一步
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
<div class="wizard-footer-right">
|
||
${currentStep < totalSteps ? `
|
||
<button class="btn btn-outline" onclick="window.__nextStep()">
|
||
下一步
|
||
<i data-lucide="arrow-right" style="width:14px;height:14px"></i>
|
||
</button>
|
||
` : `
|
||
<button class="btn btn-primary" onclick="window.__finishWizard()">
|
||
<i data-lucide="check" style="width:14px;height:14px"></i>
|
||
完成创建
|
||
</button>
|
||
`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
lucide.createIcons({ elements: [container] });
|
||
}
|
||
|
||
// ==================== 步骤控制 ====================
|
||
|
||
function collectStepData() {
|
||
if (currentStep === 1) {
|
||
formData.name = document.getElementById('kbName')?.value.trim() || '';
|
||
formData.description = document.getElementById('kbDesc')?.value.trim() || '';
|
||
}
|
||
}
|
||
|
||
window.__prevStep = function() {
|
||
if (currentStep > 1) {
|
||
collectStepData();
|
||
currentStep--;
|
||
renderPage();
|
||
}
|
||
};
|
||
|
||
window.__nextStep = function() {
|
||
collectStepData();
|
||
|
||
let errors = [];
|
||
if (currentStep === 1) {
|
||
errors = validateStep1();
|
||
} else if (currentStep === 2) {
|
||
errors = validateStep2();
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
Toast.error(errors[0]);
|
||
return;
|
||
}
|
||
|
||
if (currentStep < totalSteps) {
|
||
currentStep++;
|
||
renderPage();
|
||
}
|
||
};
|
||
|
||
window.__finishWizard = function() {
|
||
collectStepData();
|
||
|
||
// TODO: 调用 API POST /api/v1/knowledge-bases 创建知识库
|
||
console.log('创建知识库数据:', formData);
|
||
|
||
const typeMap = {
|
||
doc_search: '文档搜索',
|
||
data_query: '数据查询',
|
||
image_qa: '图片问答',
|
||
video_search: '音视频搜索',
|
||
};
|
||
const createdAt = new Date();
|
||
const pad = value => String(value).padStart(2, '0');
|
||
const createdText = `${createdAt.getFullYear()}-${pad(createdAt.getMonth() + 1)}-${pad(createdAt.getDate())} ${pad(createdAt.getHours())}:${pad(createdAt.getMinutes())}:${pad(createdAt.getSeconds())}`;
|
||
const uploadedCount = Array.isArray(formData.uploadedFiles) ? formData.uploadedFiles.length : 0;
|
||
const newKb = {
|
||
id: `kb-${Date.now()}`,
|
||
name: formData.name || '未命名知识库',
|
||
description: formData.description || '暂无描述',
|
||
docs: uploadedCount * 24,
|
||
size: uploadedCount ? `${Math.max(1, uploadedCount * 8)}MB` : '0MB',
|
||
type: formData.type || 'doc_search',
|
||
typeName: typeMap[formData.type] || '文档搜索',
|
||
status: uploadedCount ? 'processing' : 'ready',
|
||
createdAt: createdText,
|
||
updatedAt: createdText,
|
||
tags: [],
|
||
};
|
||
const catalog = Array.isArray(window.__knowledgeBaseCatalog)
|
||
? window.__knowledgeBaseCatalog
|
||
: [];
|
||
catalog.unshift(newKb);
|
||
window.__knowledgeBaseCatalog = catalog;
|
||
|
||
clearDraft();
|
||
Toast.success('知识库创建成功!');
|
||
|
||
setTimeout(() => {
|
||
navigateTo('knowledge-base');
|
||
}, 1000);
|
||
};
|
||
|
||
// ==================== 全局函数 ====================
|
||
|
||
window.__updateCharCount = function(input, max) {
|
||
const counter = input.closest('.form-group')?.querySelector('.char-counter');
|
||
if (counter) {
|
||
counter.textContent = `${input.value.length}/${max}`;
|
||
counter.classList.toggle('warning', input.value.length >= max * 0.9);
|
||
}
|
||
hasUnsavedChanges = true;
|
||
};
|
||
|
||
window.__selectKbType = function(typeId) {
|
||
formData.type = typeId;
|
||
hasUnsavedChanges = true;
|
||
document.querySelectorAll('.kb-type-card').forEach(card => {
|
||
card.classList.toggle('selected', card.dataset.type === typeId);
|
||
});
|
||
};
|
||
|
||
window.__showKbTypeHelp = function() {
|
||
createModal({
|
||
title: '知识库类型说明',
|
||
content: `
|
||
<div class="help-modal-content">
|
||
${kbTypes.map(type => `
|
||
<div class="help-modal-item">
|
||
<div class="help-modal-item-title">
|
||
<div class="help-modal-item-icon">
|
||
<i data-lucide="${type.icon}" style="width:20px;height:20px"></i>
|
||
</div>
|
||
${type.title}
|
||
</div>
|
||
<div class="help-modal-item-desc">${type.tooltip}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`,
|
||
confirmText: '',
|
||
cancelText: '关闭'
|
||
});
|
||
lucide.createIcons();
|
||
};
|
||
|
||
window.__selectSourceType = function(type) {
|
||
formData.sourceType = type;
|
||
hasUnsavedChanges = true;
|
||
const formats = getFormatsByType();
|
||
const maxFiles = 50;
|
||
const maxSize = formData.type === 'image_qa' || formData.type === 'video_search' ? '20MB' : '50MB';
|
||
document.getElementById('sourceContent').innerHTML = renderSourceContent(formats, maxFiles, maxSize);
|
||
lucide.createIcons();
|
||
};
|
||
|
||
window.__handleDragOver = function(e) {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.add('dragover');
|
||
};
|
||
|
||
window.__handleDragLeave = function(e) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
};
|
||
|
||
window.__handleDrop = function(e) {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.remove('dragover');
|
||
window.__handleFileSelect(e.dataTransfer.files);
|
||
};
|
||
|
||
window.__handleFileSelect = function(files) {
|
||
if (!files || !files.length) return;
|
||
|
||
const maxFiles = 50;
|
||
const maxSize = formData.type === 'image_qa' || formData.type === 'video_search' ? 20 * 1024 * 1024 : 50 * 1024 * 1024;
|
||
|
||
Array.from(files).forEach(file => {
|
||
if (formData.uploadedFiles.length >= maxFiles) {
|
||
Toast.warning(`最多只能上传${maxFiles}个文件`);
|
||
return;
|
||
}
|
||
if (file.size > maxSize) {
|
||
Toast.warning(`文件 ${file.name} 超过大小限制 (${maxSize / 1024 / 1024}MB)`);
|
||
return;
|
||
}
|
||
|
||
const fileObj = {
|
||
name: file.name,
|
||
size: (file.size / 1024 / 1024).toFixed(2) + 'MB',
|
||
status: 'uploading',
|
||
file: file
|
||
};
|
||
formData.uploadedFiles.push(fileObj);
|
||
hasUnsavedChanges = true;
|
||
|
||
// TODO: 调用 API POST /api/v1/files/upload 上传文件
|
||
// 模拟上传进度
|
||
setTimeout(() => {
|
||
fileObj.status = 'processing';
|
||
renderPage();
|
||
}, 500);
|
||
|
||
setTimeout(() => {
|
||
// 模拟90%成功率
|
||
fileObj.status = Math.random() > 0.1 ? 'ready' : 'error';
|
||
renderPage();
|
||
}, 2000);
|
||
});
|
||
|
||
renderPage();
|
||
};
|
||
|
||
window.__removeFile = function(index) {
|
||
formData.uploadedFiles.splice(index, 1);
|
||
hasUnsavedChanges = true;
|
||
renderPage();
|
||
};
|
||
|
||
window.__retryUpload = function(index) {
|
||
if (formData.uploadedFiles[index]) {
|
||
const fileObj = formData.uploadedFiles[index];
|
||
fileObj.status = 'uploading';
|
||
renderPage();
|
||
setTimeout(() => {
|
||
fileObj.status = 'processing';
|
||
renderPage();
|
||
}, 500);
|
||
setTimeout(() => {
|
||
fileObj.status = 'ready';
|
||
renderPage();
|
||
Toast.success(`${fileObj.name} 已重新上传并解析完成`);
|
||
}, 1500);
|
||
}
|
||
};
|
||
|
||
window.__clearAllFiles = function() {
|
||
formData.uploadedFiles = [];
|
||
hasUnsavedChanges = true;
|
||
renderPage();
|
||
};
|
||
|
||
window.__updateCategory = function(value) {
|
||
formData.category = value;
|
||
hasUnsavedChanges = true;
|
||
};
|
||
|
||
window.__selectParseMethod = function(method) {
|
||
formData.parseMethod = method;
|
||
hasUnsavedChanges = true;
|
||
document.querySelectorAll('.parse-method-option').forEach(opt => {
|
||
const isSelected = opt.textContent.includes(
|
||
method === 'inherit' ? '继承' : method === 'default' ? '默认' : '自定义'
|
||
);
|
||
opt.classList.toggle('selected', isSelected);
|
||
});
|
||
const detail = document.getElementById('parseMethodDetail');
|
||
if (detail) {
|
||
detail.innerHTML = renderParseMethodDetail();
|
||
}
|
||
};
|
||
|
||
window.__toggleParseMethod = function(type, method) {
|
||
const key = type === 'doc' ? 'docParseMethods' : type === 'image' ? 'imageParseMethods' : 'videoParseMethods';
|
||
const arr = formData[key];
|
||
const idx = arr.indexOf(method);
|
||
if (idx > -1) {
|
||
arr.splice(idx, 1);
|
||
} else {
|
||
arr.push(method);
|
||
}
|
||
hasUnsavedChanges = true;
|
||
const detail = document.getElementById('parseMethodDetail');
|
||
if (detail) {
|
||
detail.innerHTML = renderParseMethodDetail();
|
||
lucide.createIcons({ elements: [detail] });
|
||
}
|
||
};
|
||
|
||
window.__toggleAutoSync = function(checked) {
|
||
formData.autoSync = checked;
|
||
hasUnsavedChanges = true;
|
||
Toast.info(checked ? '已开启自动同步' : '已关闭自动同步');
|
||
};
|
||
|
||
window.__updateSliceConfig = function() {
|
||
formData.sliceMethod = document.getElementById('sliceMethod').value;
|
||
hasUnsavedChanges = true;
|
||
document.getElementById('sliceConfigPanel').innerHTML = renderSliceConfig();
|
||
lucide.createIcons();
|
||
};
|
||
|
||
window.__updateRangeValue = function(input, field, multiplier = 1) {
|
||
const value = parseInt(input.value);
|
||
const displayEl = document.getElementById(field + 'Value');
|
||
if (displayEl) {
|
||
displayEl.textContent = multiplier > 1 ? (value / multiplier).toFixed(2) : value;
|
||
}
|
||
formData[field] = multiplier > 1 ? value / multiplier : value;
|
||
hasUnsavedChanges = true;
|
||
};
|
||
|
||
window.__resetRetrievalParams = function() {
|
||
formData.vectorTopK = 60;
|
||
formData.keywordTopK = 60;
|
||
formData.similarityThreshold = 0.5;
|
||
formData.maxRecallCount = 10;
|
||
hasUnsavedChanges = true;
|
||
renderPage();
|
||
};
|
||
|
||
window.__toggleEnhance = function(key) {
|
||
formData['enable' + key.charAt(0).toUpperCase() + key.slice(1)] = !formData['enable' + key.charAt(0).toUpperCase() + key.slice(1)];
|
||
hasUnsavedChanges = true;
|
||
const advancedBody = document.querySelector('.advanced-config-body');
|
||
if (advancedBody) {
|
||
const options = advancedBody.querySelectorAll('.parse-method-option');
|
||
options.forEach(opt => {
|
||
if (opt.textContent.includes('Metadata抽取')) {
|
||
opt.classList.toggle('selected', formData.enableMetadataExtract);
|
||
} else if (opt.textContent.includes('Excel表头')) {
|
||
opt.classList.toggle('selected', formData.enableExcelHeader);
|
||
} else if (opt.textContent.includes('多轮对话')) {
|
||
opt.classList.toggle('selected', formData.enableMultiTurn);
|
||
} else if (opt.textContent.includes('OCR')) {
|
||
opt.classList.toggle('selected', formData.enableOCR);
|
||
} else if (opt.textContent.includes('关键词')) {
|
||
opt.classList.toggle('selected', formData.enableAutoKeywords);
|
||
} else if (opt.textContent.includes('标签')) {
|
||
opt.classList.toggle('selected', formData.enableAutoTags);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
window.__showTooltip = function(id) {
|
||
const tooltip = document.getElementById(id);
|
||
if (tooltip) {
|
||
tooltip.style.display = tooltip.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
};
|
||
|
||
window.__toggleFileSelect = function(el, filename) {
|
||
const checkbox = el.querySelector('input[type="checkbox"]');
|
||
checkbox.checked = !checkbox.checked;
|
||
el.classList.toggle('selected', checkbox.checked);
|
||
Toast.info(checkbox.checked ? `已选择: ${filename}` : `已取消: ${filename}`);
|
||
};
|
||
|
||
window.__loadCategoryFiles = function(category) {
|
||
// TODO: 调用 API GET /api/v1/categories/:id/files
|
||
createModal({
|
||
title: `${category} 文件列表`,
|
||
size: 'lg',
|
||
content: `
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
${['儿童科学百科.pdf', '科学小实验.docx', '物理入门.xlsx'].map((name, index) => `
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px;border:1px solid var(--border);border-radius:var(--radius)">
|
||
<div>
|
||
<div style="font-weight:600">${name}</div>
|
||
<div style="font-size:12px;color:var(--text-muted)">mock-file-${index + 1} · ${(index + 1) * 2}.4MB</div>
|
||
</div>
|
||
<button class="btn btn-outline btn-sm" onclick="Toast.success('已选择 ${name}')">选择</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`,
|
||
confirmText: '',
|
||
cancelText: '关闭'
|
||
});
|
||
};
|
||
|
||
window.__showCategoryDrawer = function() {
|
||
const renderCategoryRows = () => {
|
||
const list = document.getElementById('categoryManageList');
|
||
if (!list) return;
|
||
list.innerHTML = categories.map(category => `
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px;border:1px solid var(--border);border-radius:var(--radius)">
|
||
<div>
|
||
<div style="font-weight:600">${category.name}</div>
|
||
<div style="font-size:12px;color:var(--text-muted)">${category.fileCount} 个文件 · ${category.id}</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn btn-outline btn-sm" onclick="window.__chooseCategoryFromModal('${category.name}')">选择</button>
|
||
${category.id === 'cat-001' ? '' : `
|
||
<button class="btn btn-ghost btn-sm" onclick="window.__removeCategoryFromModal('${category.id}')">
|
||
<i data-lucide="trash-2" style="width:14px;height:14px"></i>
|
||
</button>
|
||
`}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
lucide.createIcons({ elements: list });
|
||
};
|
||
|
||
createModal({
|
||
title: '类目管理',
|
||
size: 'lg',
|
||
content: `
|
||
<div style="display:flex;gap:10px;margin-bottom:14px">
|
||
<input class="form-control" id="newCategoryName" placeholder="输入新类目名称">
|
||
<button class="btn btn-primary" type="button" onclick="window.__addCategoryFromModal()">添加</button>
|
||
</div>
|
||
<div id="categoryManageList" style="display:flex;flex-direction:column;gap:10px"></div>
|
||
`,
|
||
confirmText: '',
|
||
cancelText: '关闭'
|
||
});
|
||
|
||
window.__addCategoryFromModal = function() {
|
||
const input = document.getElementById('newCategoryName');
|
||
const name = input?.value.trim();
|
||
if (!name) {
|
||
Toast.warning('请输入类目名称');
|
||
return;
|
||
}
|
||
categories.push({
|
||
id: `cat-${Date.now()}`,
|
||
name,
|
||
fileCount: 0,
|
||
});
|
||
input.value = '';
|
||
renderCategoryRows();
|
||
Toast.success('类目已添加');
|
||
};
|
||
|
||
window.__removeCategoryFromModal = function(id) {
|
||
const index = categories.findIndex(category => category.id === id);
|
||
if (index !== -1) {
|
||
categories.splice(index, 1);
|
||
renderCategoryRows();
|
||
Toast.success('类目已删除');
|
||
}
|
||
};
|
||
|
||
window.__chooseCategoryFromModal = function(name) {
|
||
formData.category = name;
|
||
hasUnsavedChanges = true;
|
||
const select = document.getElementById('categorySelect');
|
||
if (select && Array.from(select.options).some(option => option.value === name)) {
|
||
select.value = name;
|
||
}
|
||
Toast.success(`已选择类目:${name}`);
|
||
};
|
||
|
||
setTimeout(renderCategoryRows, 50);
|
||
};
|
||
|
||
window.__saveDraft = saveDraft;
|
||
|
||
window.__confirmLeave = function() {
|
||
if (hasUnsavedChanges) {
|
||
createModal({
|
||
title: '确认离开',
|
||
content: `
|
||
<div class="confirm-leave-modal">
|
||
<p>确定要离开吗?</p>
|
||
<p class="warning-text">您有未保存的更改,离开后将丢失。</p>
|
||
</div>
|
||
`,
|
||
confirmText: '离开',
|
||
confirmClass: 'btn-danger',
|
||
onConfirm: () => {
|
||
navigateTo('knowledge-base');
|
||
}
|
||
});
|
||
} else {
|
||
navigateTo('knowledge-base');
|
||
}
|
||
};
|
||
|
||
// ==================== 初始化 ====================
|
||
|
||
loadDraft();
|
||
renderPage();
|
||
|
||
// 离开页面确认
|
||
window.addEventListener('beforeunload', function(e) {
|
||
if (hasUnsavedChanges) {
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
}
|
||
});
|
||
});
|