nodebookls/templates/settings.html
2025-10-29 13:56:24 +08:00

808 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - TXT版轻量NotebookLM</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.nav {
text-align: center;
margin-bottom: 30px;
}
.nav a {
display: inline-block;
padding: 10px 20px;
margin: 0 10px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
}
.nav a:hover {
background-color: #45a049;
}
.nav a.settings {
background-color: #2196F3;
}
.nav a.settings:hover {
background-color: #0b7dda;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.section h2 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 5px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
button:hover {
background-color: #45a049;
}
.btn-secondary {
background-color: #2196F3;
}
.btn-secondary:hover {
background-color: #0b7dda;
}
.btn-danger {
background-color: #f44336;
}
.btn-danger:hover {
background-color: #d32f2f;
}
.alert {
padding: 15px;
margin: 15px 0;
border-radius: 5px;
}
.alert.info {
background-color: #e3f2fd;
border-left: 4px solid #2196F3;
color: #0d47a1;
}
.alert.success {
background-color: #e8f5e9;
border-left: 4px solid #4CAF50;
color: #1b5e20;
}
.alert.warning {
background-color: #fff3e0;
border-left: 4px solid #FF9800;
color: #e65100;
}
.alert.error {
background-color: #ffebee;
border-left: 4px solid #f44336;
color: #b71c1c;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 15px;
}
.nav a {
display: block;
margin: 5px 0;
}
}
</style>
</head>
<body>
<div class="container">
<h1>TXT版轻量NotebookLM 设置</h1>
<div class="nav">
<a href="/">主页面</a>
<a href="/settings" class="settings active">设置</a>
<a href="/knowledge_bases" class="kb">知识库管理</a>
</div>
<div id="messageArea" class="alert hidden"></div>
<div id="messageArea" class="hidden"></div>
<!-- 系统配置 -->
<div class="section">
<h2>系统配置</h2>
<div class="form-group">
<label for="maxFiles">最大上传文件数:</label>
<input type="number" id="maxFiles" min="1" max="100" value="20">
</div>
<div class="form-group">
<label for="topK">检索返回结果数量 (Top-K):</label>
<input type="number" id="topK" min="1" max="50" value="8">
</div>
<div class="form-group">
<label for="batchSize">批处理大小:</label>
<input type="number" id="batchSize" min="1" max="100" value="50">
</div>
<button id="saveSystemConfig">保存系统配置</button>
</div>
<!-- 生成配置 -->
<div class="section">
<h2>生成配置</h2>
<div class="form-group">
<label for="defaultStyle">默认文案风格:</label>
<select id="defaultStyle">
<option value="通用文案">通用文案</option>
<option value="小红书种草风">小红书种草风</option>
<option value="官方通告">官方通告</option>
<option value="知乎科普">知乎科普</option>
<option value="微博热点">微博热点</option>
</select>
</div>
<div class="form-group">
<label for="defaultMinLength">默认最小字数:</label>
<input type="number" id="defaultMinLength" min="10" max="1000" value="50">
</div>
<div class="form-group">
<label for="defaultMaxLength">默认最大字数:</label>
<input type="number" id="defaultMaxLength" min="50" max="2000" value="200">
</div>
<div class="form-group">
<label for="temperature">生成温度 (0.0-1.0):</label>
<input type="number" id="temperature" min="0" max="1" step="0.01" value="0.25">
</div>
<button id="saveGenerationConfig">保存生成配置</button>
</div>
<!-- 检索配置 -->
<div class="section">
<h2>检索配置</h2>
<div class="form-group">
<label for="defaultSearchType">默认检索方式:</label>
<select id="defaultSearchType">
<option value="hybrid">混合检索(BM25+向量)</option>
<option value="vector">仅向量检索</option>
</select>
</div>
<div class="form-group">
<label for="bm25Weight">BM25权重 (混合检索时):</label>
<input type="number" id="bm25Weight" min="0" max="1" step="0.01" value="0.5">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="includeContext" checked> 默认包含上下文
</label>
</div>
<button id="saveRetrievalConfig">保存检索配置</button>
</div>
<!-- 导出配置 -->
<div class="section">
<h2>导出配置</h2>
<div class="form-group">
<label for="defaultExportFormat">默认导出格式:</label>
<select id="defaultExportFormat">
<option value="markdown">Markdown</option>
<option value="docx">DOCX</option>
</select>
</div>
<button id="saveExportConfig">保存导出配置</button>
</div>
<!-- 日志配置 -->
<div class="section">
<h2>日志配置</h2>
<div class="form-group">
<label>
<input type="checkbox" id="enableLogging" checked> 启用生成日志记录
</label>
</div>
<button id="clearLogs" class="btn-danger">清空所有日志</button>
<button id="viewLogs" class="btn-secondary">查看日志</button>
<button id="saveLogConfig">保存日志配置</button>
</div>
<!-- API配置 -->
<div class="section">
<h2>API配置</h2>
<div class="form-group">
<label for="embeddingProvider">嵌入模型提供商:</label>
<select id="embeddingProvider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="qwen">通义千问</option>
<option value="openrouter">OpenRouter</option>
<option value="siliconflow">硅基流动</option>
</select>
</div>
<div class="form-group">
<label for="embeddingModel">嵌入模型:</label>
<select id="embeddingModel">
<!-- 模型选项将根据提供商动态更新 -->
</select>
</div>
<div class="form-group">
<label for="embeddingApiBase">嵌入模型API地址:</label>
<input type="text" id="embeddingApiBase" value="https://api.openai.com/v1">
</div>
<div class="form-group">
<label for="generationProvider">生成模型提供商:</label>
<select id="generationProvider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="qwen">通义千问</option>
<option value="openrouter">OpenRouter</option>
<option value="siliconflow">硅基流动</option>
</select>
</div>
<div class="form-group">
<label for="generationModel">生成模型:</label>
<select id="generationModel">
<!-- 模型选项将根据提供商动态更新 -->
</select>
</div>
<div class="form-group">
<label for="generationApiBase">生成模型API地址:</label>
<input type="text" id="generationApiBase" value="https://api.openai.com/v1">
</div>
<button id="testModelConnection" class="btn-secondary">测试模型连接</button>
<div id="modelTestResult" class="alert hidden"></div>
<button id="saveApiConfig">保存API配置</button>
</div>
<!-- 第三方API密钥配置 -->
<div class="section">
<h2>第三方API密钥配置</h2>
<div class="form-group">
<label for="generationApiBase">生成模型API地址:</label>
<input type="text" id="generationApiBase" value="https://api.openai.com/v1">
</div>
<button id="saveApiKeyConfig">保存API密钥配置</button>
</div>
<!-- 第三方API密钥配置 -->
<div class="section">
<h2>第三方API密钥配置</h2>
<div class="form-group">
<label for="openrouterApiKey">OpenRouter API密钥:</label>
<input type="password" id="openrouterApiKey" placeholder="请输入OpenRouter API密钥">
</div>
<div class="form-group">
<label for="siliconflowApiKey">硅基流动API密钥:</label>
<input type="password" id="siliconflowApiKey" placeholder="请输入硅基流动API密钥">
</div>
<button id="saveApiKeyConfig">保存API密钥配置</button>
</div>
<!-- 模型管理 -->
<div class="section">
<h2>模型管理</h2>
<div class="form-group">
<p>当前支持的模型提供商:</p>
<ul>
<li><strong>OpenAI</strong>: 支持GPT系列模型</li>
<li><strong>Anthropic</strong>: 支持Claude系列模型</li>
<li><strong>通义千问</strong>: 支持Qwen系列模型</li>
<li><strong>OpenRouter</strong>: 支持数百种AI模型的统一API网关</li>
<li><strong>硅基流动</strong>: 支持国内新兴的模型服务提供商</li>
</ul>
</div>
<div class="form-group">
<p>推荐模型配置:</p>
<ul>
<li><strong>嵌入模型</strong>: text-embedding-ada-002 (OpenAI) 或 SiliconFlow/deepseek-ai/DeepSeek-V3</li>
<li><strong>生成模型</strong>: gpt-3.5-turbo (OpenAI) 或 openrouter/anthropic/claude-3.5-sonnet</li>
</ul>
</div>
<button id="loadModelConfig">加载推荐模型配置</button>
</div>
</div>
<script>
// 定义模型映射关系
const modelMapping = {
'openai': {
'embedding': ['text-embedding-ada-002'],
'generation': ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o']
},
'anthropic': {
'embedding': [],
'generation': ['claude-3-haiku', 'claude-3-sonnet', 'claude-3-opus']
},
'qwen': {
'embedding': [],
'generation': ['qwen-turbo', 'qwen-plus', 'qwen-max']
},
'openrouter': {
'embedding': [],
'generation': ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'google/gemini-pro', 'meta-llama/llama-3.1-70b-instruct']
},
'siliconflow': {
'embedding': ['deepseek-ai/DeepSeek-V3'],
'generation': ['deepseek-ai/DeepSeek-R1', 'Qwen/Qwen3-72B', 'Qwen/Qwen3-14B']
}
};
// 更新模型下拉框选项
function updateModelOptions(providerSelectId, modelSelectId, modelType) {
const provider = document.getElementById(providerSelectId).value;
const modelSelect = document.getElementById(modelSelectId);
// 清空现有选项
modelSelect.innerHTML = '';
// 添加新选项
const models = modelMapping[provider][modelType] || [];
models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
modelSelect.appendChild(option);
});
}
// 更新API地址
function updateApiBase(providerSelectId, apiBaseId) {
const provider = document.getElementById(providerSelectId).value;
const apiBaseInput = document.getElementById(apiBaseId);
// 根据提供商设置默认API地址
const defaultApiBases = {
'openai': 'https://api.openai.com/v1',
'anthropic': 'https://api.anthropic.com/v1',
'qwen': 'https://dashscope.aliyuncs.com/api/v1',
'openrouter': 'https://openrouter.ai/api/v1',
'siliconflow': 'https://api.siliconflow.cn/v1'
};
apiBaseInput.value = defaultApiBases[provider] || '';
}
// 页面加载时获取当前配置
document.addEventListener('DOMContentLoaded', async function() {
try {
const response = await fetch('/api/config');
if (response.ok) {
const config = await response.json();
// 填充表单字段
document.getElementById('maxFiles').value = config.max_files;
document.getElementById('topK').value = config.top_k;
document.getElementById('batchSize').value = config.batch_size;
document.getElementById('defaultStyle').value = config.default_style;
document.getElementById('defaultMinLength').value = config.default_min_length;
document.getElementById('defaultMaxLength').value = config.default_max_length;
document.getElementById('temperature').value = config.temperature;
document.getElementById('defaultSearchType').value = config.default_search_type;
document.getElementById('bm25Weight').value = config.bm25_weight;
document.getElementById('includeContext').checked = config.include_context;
document.getElementById('defaultExportFormat').value = config.default_export_format;
document.getElementById('enableLogging').checked = config.enable_logging;
document.getElementById('embeddingProvider').value = config.embedding_provider || 'openai';
document.getElementById('generationProvider').value = config.generation_provider || 'openai';
document.getElementById('openrouterApiKey').value = config.openrouter_api_key || '';
document.getElementById('siliconflowApiKey').value = config.siliconflow_api_key || '';
// 更新模型选项
updateModelOptions('embeddingProvider', 'embeddingModel', 'embedding');
updateModelOptions('generationProvider', 'generationModel', 'generation');
// 设置模型值
document.getElementById('embeddingModel').value = config.embedding_model || 'text-embedding-ada-002';
document.getElementById('generationModel').value = config.generation_model || 'gpt-3.5-turbo';
// 更新API地址
updateApiBase('embeddingProvider', 'embeddingApiBase');
updateApiBase('generationProvider', 'generationApiBase');
// 设置API地址值
document.getElementById('embeddingApiBase').value = config.openai_api_base || 'https://api.openai.com/v1';
document.getElementById('generationApiBase').value = config.openai_api_base || 'https://api.openai.com/v1';
} else {
throw new Error('获取配置失败');
}
} catch (error) {
showMessage('加载配置失败: ' + error.message, 'error');
}
// 添加提供商选择事件监听器
document.getElementById('embeddingProvider').addEventListener('change', function() {
updateModelOptions('embeddingProvider', 'embeddingModel', 'embedding');
updateApiBase('embeddingProvider', 'embeddingApiBase');
});
document.getElementById('generationProvider').addEventListener('change', function() {
updateModelOptions('generationProvider', 'generationModel', 'generation');
updateApiBase('generationProvider', 'generationApiBase');
});
});
// 保存系统配置
document.getElementById('saveSystemConfig').addEventListener('click', async function() {
const config = {
maxFiles: parseInt(document.getElementById('maxFiles').value),
topK: parseInt(document.getElementById('topK').value),
batchSize: parseInt(document.getElementById('batchSize').value)
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
max_files: config.maxFiles,
top_k: config.topK,
batch_size: config.batchSize
})
});
if (response.ok) {
showMessage('系统配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存系统配置失败: ' + error.message, 'error');
}
});
// 保存生成配置
document.getElementById('saveGenerationConfig').addEventListener('click', async function() {
const config = {
defaultStyle: document.getElementById('defaultStyle').value,
defaultMinLength: parseInt(document.getElementById('defaultMinLength').value),
defaultMaxLength: parseInt(document.getElementById('defaultMaxLength').value),
temperature: parseFloat(document.getElementById('temperature').value)
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
default_style: config.defaultStyle,
default_min_length: config.defaultMinLength,
default_max_length: config.defaultMaxLength,
temperature: config.temperature
})
});
if (response.ok) {
showMessage('生成配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存生成配置失败: ' + error.message, 'error');
}
});
// 保存检索配置
document.getElementById('saveRetrievalConfig').addEventListener('click', async function() {
const config = {
defaultSearchType: document.getElementById('defaultSearchType').value,
bm25Weight: parseFloat(document.getElementById('bm25Weight').value),
includeContext: document.getElementById('includeContext').checked
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
default_search_type: config.defaultSearchType,
bm25_weight: config.bm25Weight,
include_context: config.includeContext
})
});
if (response.ok) {
showMessage('检索配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存检索配置失败: ' + error.message, 'error');
}
});
// 保存导出配置
document.getElementById('saveExportConfig').addEventListener('click', async function() {
const config = {
defaultExportFormat: document.getElementById('defaultExportFormat').value
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
default_export_format: config.defaultExportFormat
})
});
if (response.ok) {
showMessage('导出配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存导出配置失败: ' + error.message, 'error');
}
});
// 保存日志配置
document.getElementById('saveLogConfig').addEventListener('click', async function() {
const config = {
enableLogging: document.getElementById('enableLogging').checked
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enable_logging: config.enableLogging
})
});
if (response.ok) {
showMessage('日志配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存日志配置失败: ' + error.message, 'error');
}
});
// 清空日志
document.getElementById('clearLogs').addEventListener('click', async function() {
if (confirm('确定要清空所有日志吗?此操作不可恢复。')) {
try {
const response = await fetch('/logs', {
method: 'DELETE'
});
if (response.ok) {
showMessage('日志已清空', 'success');
} else {
throw new Error('清空日志失败');
}
} catch (error) {
showMessage('清空日志失败: ' + error.message, 'error');
}
}
});
// 查看日志
document.getElementById('viewLogs').addEventListener('click', function() {
// 这里应该跳转到日志查看页面或显示日志模态框
alert('查看日志功能待实现');
});
// 保存API配置
document.getElementById('saveApiConfig').addEventListener('click', async function() {
const config = {
embeddingProvider: document.getElementById('embeddingProvider').value,
embeddingModel: document.getElementById('embeddingModel').value,
embeddingApiBase: document.getElementById('embeddingApiBase').value,
generationProvider: document.getElementById('generationProvider').value,
generationModel: document.getElementById('generationModel').value,
generationApiBase: document.getElementById('generationApiBase').value,
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
embedding_provider: config.embeddingProvider,
embedding_model: config.embeddingModel,
openai_api_base: config.embeddingApiBase, // 注意:这里应该根据提供商设置正确的配置项
generation_provider: config.generationProvider,
generation_model: config.generationModel,
openai_api_base: config.generationApiBase, // 注意:这里应该根据提供商设置正确的配置项
})
});
if (response.ok) {
showMessage('API配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存API配置失败: ' + error.message, 'error');
}
});
// 测试模型连接
document.getElementById('testModelConnection').addEventListener('click', async function() {
// 获取表单数据
const providerType = document.getElementById('generationProvider').value;
const modelName = document.getElementById('generationModel').value;
// 根据提供商获取对应的API密钥
let apiKey = '';
if (providerType === 'openrouter') {
apiKey = document.getElementById('openrouterApiKey').value;
} else if (providerType === 'siliconflow') {
apiKey = document.getElementById('siliconflowApiKey').value;
} else {
// 对于其他提供商,可能需要从环境变量获取
// 这里暂时留空,实际应用中可能需要其他处理方式
}
const apiBase = document.getElementById('generationApiBase').value;
// 显示测试中状态
const testResult = document.getElementById('modelTestResult');
testResult.textContent = '测试中...';
testResult.className = 'alert info';
testResult.classList.remove('hidden');
try {
// 发送测试请求
const formData = new FormData();
formData.append('provider_type', providerType);
formData.append('model_name', modelName);
formData.append('api_key', apiKey);
formData.append('api_base', apiBase);
const response = await fetch('/test_model_connection', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
testResult.textContent = result.message;
testResult.className = 'alert success';
} else {
throw new Error(result.detail || '测试失败');
}
} catch (error) {
testResult.textContent = '测试失败: ' + error.message;
testResult.className = 'alert error';
}
});
// 保存API密钥配置
document.getElementById('saveApiKeyConfig').addEventListener('click', async function() {
const config = {
openrouterApiKey: document.getElementById('openrouterApiKey').value,
siliconflowApiKey: document.getElementById('siliconflowApiKey').value
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
openrouter_api_key: config.openrouterApiKey,
siliconflow_api_key: config.siliconflowApiKey
})
});
if (response.ok) {
showMessage('API密钥配置保存成功', 'success');
} else {
throw new Error('保存失败');
}
} catch (error) {
showMessage('保存API密钥配置失败: ' + error.message, 'error');
}
});
// 加载推荐模型配置
document.getElementById('loadModelConfig').addEventListener('click', function() {
// 这里可以实现加载推荐模型配置的逻辑
alert('推荐模型配置加载功能待实现');
});
// 显示消息
function showMessage(message, type) {
const messageArea = document.getElementById('messageArea');
messageArea.textContent = message;
messageArea.className = type;
messageArea.classList.remove('hidden');
// 3秒后自动隐藏消息
setTimeout(() => {
messageArea.classList.add('hidden');
}, 3000);
}
</script>
</body>
</html>