1004 lines
38 KiB
HTML
1004 lines
38 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;
|
||
}
|
||
.nav a.kb {
|
||
background-color: #9C27B0;
|
||
}
|
||
.nav a.kb:hover {
|
||
background-color: #7B1FA2;
|
||
}
|
||
.section {
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
}
|
||
.section h2 {
|
||
margin-top: 0;
|
||
color: #333;
|
||
}
|
||
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;
|
||
}
|
||
button:disabled {
|
||
background-color: #cccccc;
|
||
cursor: not-allowed;
|
||
}
|
||
.btn-secondary {
|
||
background-color: #2196F3;
|
||
}
|
||
.btn-secondary:hover {
|
||
background-color: #0b7dda;
|
||
}
|
||
.btn-warning {
|
||
background-color: #FF9800;
|
||
}
|
||
.btn-warning:hover {
|
||
background-color: #e68a00;
|
||
}
|
||
.btn-danger {
|
||
background-color: #f44336;
|
||
}
|
||
.btn-danger:hover {
|
||
background-color: #d32f2f;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
}
|
||
textarea, input, select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
margin: 5px 0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
box-sizing: border-box;
|
||
}
|
||
.result {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #f9f9f9;
|
||
border-left: 4px solid #4CAF50;
|
||
border-radius: 5px;
|
||
}
|
||
.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;
|
||
}
|
||
.hallucination-warning {
|
||
background-color: #fff3e0;
|
||
border-left: 4px solid #FF9800;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 5px;
|
||
}
|
||
.progress {
|
||
height: 20px;
|
||
background-color: #f0f0f0;
|
||
border-radius: 10px;
|
||
margin: 10px 0;
|
||
}
|
||
.progress-bar {
|
||
height: 100%;
|
||
background-color: #4CAF50;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
line-height: 20px;
|
||
color: white;
|
||
font-size: 12px;
|
||
}
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
.search-results {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #fff3e0;
|
||
border-left: 4px solid #FF9800;
|
||
border-radius: 5px;
|
||
}
|
||
.modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
z-index: 9999;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.modal-content {
|
||
background-color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
width: 80%;
|
||
max-width: 800px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
}
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
}
|
||
.close {
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
}
|
||
.close:hover {
|
||
color: #333;
|
||
}
|
||
@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="/" class="active">主页面</a>
|
||
<a href="/settings" class="settings">设置</a>
|
||
<a href="/knowledge_bases" class="kb">知识库管理</a>
|
||
</div>
|
||
|
||
<div id="globalAlert" class="alert hidden"></div>
|
||
|
||
<!-- 知识库选择和问题输入 -->
|
||
<div class="section">
|
||
<h2>基于知识库提问</h2>
|
||
<p>选择知识库并向AI提问,系统将基于知识库内容为您提供答案</p>
|
||
|
||
<div class="form-group">
|
||
<label for="kbSelect">选择知识库:</label>
|
||
<select id="kbSelect">
|
||
<option value="">请选择知识库</option>
|
||
<!-- 知识库选项将通过JavaScript动态加载 -->
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="questionInput">请输入您的问题:</label>
|
||
<textarea id="questionInput" rows="3" placeholder="例如:根据文档内容,项目的主要功能是什么?"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="styleSelect">回答风格:</label>
|
||
<select id="styleSelect">
|
||
<option value="通用文案">通用文案</option>
|
||
<option value="小红书种草风">小红书种草风</option>
|
||
<option value="官方通告">官方通告</option>
|
||
<option value="知乎科普">知乎科普</option>
|
||
<option value="微博热点">微博热点</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>回答长度:</label>
|
||
<input type="number" id="minLength" value="50" min="10" max="500"> ~
|
||
<input type="number" id="maxLength" value="500" min="50" max="2000"> 字符
|
||
</div>
|
||
|
||
<button id="askBtn" disabled>获取答案</button>
|
||
<button id="refreshKbList">刷新知识库列表</button>
|
||
|
||
<div id="generateProgress" class="progress hidden">
|
||
<div class="progress-bar" id="generateProgressBar" style="width: 0%">0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI回答结果 -->
|
||
<div class="section">
|
||
<h2>AI回答</h2>
|
||
<div id="answerResult" class="result hidden">
|
||
<div id="answerContent"></div>
|
||
<div style="margin-top: 15px;">
|
||
<button id="exportBtn" class="btn-secondary hidden">导出为Markdown</button>
|
||
<button id="exportDocxBtn" class="btn-secondary hidden">导出为DOCX</button>
|
||
<button id="editBtn" class="btn-warning hidden">人工微调</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 相关知识内容 -->
|
||
<div class="section">
|
||
<h2>支持回答的知识内容</h2>
|
||
<div id="searchResults" class="search-results hidden">
|
||
<div id="relatedContent"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文本编辑器模态框 -->
|
||
<div id="editorModal" class="modal hidden">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>人工微调</h3>
|
||
<span class="close" onclick="cancelEdit()">×</span>
|
||
</div>
|
||
<textarea id="editorTextarea" style="width: 100%; height: 300px;"></textarea>
|
||
<div style="margin-top: 10px; text-align: right;">
|
||
<button id="saveEditBtn" class="btn-secondary">保存</button>
|
||
<button id="cancelEditBtn" class="btn-danger">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let lastGeneratedResult = null;
|
||
let currentEditingText = "";
|
||
let selectedKb = "";
|
||
|
||
// DOM元素
|
||
const kbSelect = document.getElementById('kbSelect');
|
||
const questionInput = document.getElementById('questionInput');
|
||
const askBtn = document.getElementById('askBtn');
|
||
const refreshKbListBtn = document.getElementById('refreshKbList');
|
||
const generateProgress = document.getElementById('generateProgress');
|
||
const generateProgressBar = document.getElementById('generateProgressBar');
|
||
const answerResult = document.getElementById('answerResult');
|
||
const answerContent = document.getElementById('answerContent');
|
||
const exportBtn = document.getElementById('exportBtn');
|
||
const exportDocxBtn = document.getElementById('exportDocxBtn');
|
||
const editBtn = document.getElementById('editBtn');
|
||
const searchResults = document.getElementById('searchResults');
|
||
const relatedContent = document.getElementById('relatedContent');
|
||
|
||
// 编辑器元素
|
||
const editorModal = document.getElementById('editorModal');
|
||
const editorTextarea = document.getElementById('editorTextarea');
|
||
const saveEditBtn = document.getElementById('saveEditBtn');
|
||
const cancelEditBtn = document.getElementById('cancelEditBtn');
|
||
|
||
// 事件监听器
|
||
kbSelect.addEventListener('change', handleKbSelect);
|
||
questionInput.addEventListener('input', handleQuestionInput);
|
||
askBtn.addEventListener('click', askQuestion);
|
||
refreshKbListBtn.addEventListener('click', loadKnowledgeBases);
|
||
exportBtn.addEventListener('click', exportToMarkdown);
|
||
exportDocxBtn.addEventListener('click', exportToDocx);
|
||
editBtn.addEventListener('click', openEditor);
|
||
|
||
// 编辑器事件监听器
|
||
saveEditBtn.addEventListener('click', saveEdit);
|
||
cancelEditBtn.addEventListener('click', cancelEdit);
|
||
|
||
// 点击模态框背景关闭编辑器
|
||
editorModal.addEventListener('click', (e) => {
|
||
if (e.target === editorModal) {
|
||
cancelEdit();
|
||
}
|
||
});
|
||
|
||
// 页面加载时获取知识库列表
|
||
document.addEventListener('DOMContentLoaded', async function() {
|
||
await loadKnowledgeBases();
|
||
});
|
||
|
||
// 加载知识库列表
|
||
async function loadKnowledgeBases() {
|
||
try {
|
||
const response = await fetch('/knowledge_bases');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
displayKnowledgeBases(data.knowledge_bases);
|
||
} else {
|
||
throw new Error('获取知识库列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载知识库列表失败:', error);
|
||
showMessage('加载知识库列表失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 显示知识库列表
|
||
function displayKnowledgeBases(knowledgeBases) {
|
||
kbSelect.innerHTML = '<option value="">请选择知识库</option>';
|
||
|
||
knowledgeBases.forEach(kb => {
|
||
const option = document.createElement('option');
|
||
option.value = kb.name;
|
||
option.textContent = `${kb.name} (${kb.document_count} 个文档)`;
|
||
kbSelect.appendChild(option);
|
||
});
|
||
|
||
// 如果之前选择了知识库,重新选择它
|
||
if (selectedKb) {
|
||
kbSelect.value = selectedKb;
|
||
askBtn.disabled = !questionInput.value.trim();
|
||
}
|
||
}
|
||
|
||
// 处理知识库选择
|
||
function handleKbSelect() {
|
||
selectedKb = kbSelect.value;
|
||
askBtn.disabled = !selectedKb || !questionInput.value.trim();
|
||
}
|
||
|
||
// 处理问题输入
|
||
function handleQuestionInput() {
|
||
askBtn.disabled = !selectedKb || !questionInput.value.trim();
|
||
}
|
||
|
||
// 提问
|
||
async function askQuestion() {
|
||
const question = questionInput.value.trim();
|
||
if (!selectedKb || !question) {
|
||
alert('请选择知识库并输入问题');
|
||
return;
|
||
}
|
||
|
||
// 显示进度条
|
||
generateProgress.classList.remove('hidden');
|
||
askBtn.disabled = true;
|
||
|
||
try {
|
||
// 构造表单数据
|
||
const formData = new FormData();
|
||
formData.append('query', question);
|
||
formData.append('style', document.getElementById('styleSelect').value);
|
||
formData.append('min_length', document.getElementById('minLength').value);
|
||
formData.append('max_length', document.getElementById('maxLength').value);
|
||
formData.append('knowledge_base', selectedKb);
|
||
|
||
// 调用新的问答API
|
||
const response = await fetch('/ask', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
// 保存结果用于导出
|
||
lastGeneratedResult = {
|
||
query: result.query,
|
||
generated_text: result.answer,
|
||
knowledge_base: selectedKb
|
||
};
|
||
|
||
// 显示AI回答
|
||
let answerHTML = `
|
||
<h3>问题: ${result.query}</h3>
|
||
<p><strong>基于知识库 "${selectedKb}" 的回答:</strong></p>
|
||
<p>${result.answer.replace(/\n/g, '<br>')}</p>
|
||
`;
|
||
|
||
// 显示幻觉警告信息
|
||
if (result.hallucination_warnings &&
|
||
(result.hallucination_warnings.keywords.length > 0 ||
|
||
result.hallucination_warnings.entities.length > 0)) {
|
||
let warningHTML = '<div class="hallucination-warning">';
|
||
warningHTML += '<p class="warning">⚠️ 检测到可能的幻觉内容:</p><ul>';
|
||
|
||
if (result.hallucination_warnings.keywords.length > 0) {
|
||
warningHTML += `<li><strong>幻觉关键词:</strong> ${result.hallucination_warnings.keywords.join(', ')}</li>`;
|
||
}
|
||
|
||
if (result.hallucination_warnings.entities.length > 0) {
|
||
warningHTML += `<li><strong>幻觉实体:</strong> ${result.hallucination_warnings.entities.join(', ')}</li>`;
|
||
}
|
||
|
||
warningHTML += '</ul><p class="warning">请核对原文确认表述准确性</p></div>';
|
||
answerHTML += warningHTML;
|
||
}
|
||
|
||
answerContent.innerHTML = answerHTML;
|
||
answerResult.classList.remove('hidden');
|
||
exportBtn.classList.remove('hidden');
|
||
editBtn.classList.remove('hidden');
|
||
|
||
// 显示支持回答的知识内容
|
||
if (result.related_content && result.related_content.length > 0) {
|
||
displayRelatedContent(result.related_content);
|
||
} else {
|
||
searchResults.classList.add('hidden');
|
||
}
|
||
} else {
|
||
throw new Error(result.detail || '获取答案失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('提问错误:', error);
|
||
answerContent.innerHTML = `<p class="error">获取答案失败: ${error.message}</p>`;
|
||
answerResult.classList.remove('hidden');
|
||
} finally {
|
||
// 隐藏进度条
|
||
generateProgress.classList.add('hidden');
|
||
askBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 显示相关知识内容
|
||
function displayRelatedContent(contents) {
|
||
let html = '<h3>支持回答的知识内容:</h3>';
|
||
contents.forEach((item, index) => {
|
||
html += `
|
||
<div style="margin: 15px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px;">
|
||
<p><strong>内容片段 ${index + 1} (${item.metadata.segment_id})</strong></p>
|
||
<p><strong>相关性得分:</strong> ${(item.score * 100).toFixed(2)}%</p>
|
||
<p>${item.content}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
relatedContent.innerHTML = html;
|
||
searchResults.classList.remove('hidden');
|
||
}
|
||
|
||
// 导出为Markdown
|
||
async function exportToMarkdown() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可导出的内容');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建FormData
|
||
const formData = new FormData();
|
||
formData.append('query', lastGeneratedResult.query);
|
||
formData.append('generated_text', lastGeneratedResult.generated_text);
|
||
formData.append('knowledge_base', lastGeneratedResult.knowledge_base);
|
||
|
||
// 发送导出请求
|
||
const response = await fetch('/export_markdown_qa', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = response.headers.get('content-disposition')
|
||
.split('filename=')[1]
|
||
.replace(/"/g, '');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
alert('导出成功!');
|
||
} else {
|
||
throw new Error('导出失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出错误:', error);
|
||
alert('导出失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 导出为DOCX
|
||
async function exportToDocx() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可导出的内容');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建FormData
|
||
const formData = new FormData();
|
||
formData.append('query', lastGeneratedResult.query);
|
||
formData.append('generated_text', lastGeneratedResult.generated_text);
|
||
formData.append('knowledge_base', lastGeneratedResult.knowledge_base);
|
||
|
||
// 发送导出请求
|
||
const response = await fetch('/export_docx_qa', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = response.headers.get('content-disposition')
|
||
.split('filename=')[1]
|
||
.replace(/"/g, '');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
alert('DOCX导出成功!');
|
||
} else {
|
||
throw new Error('导出失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出错误:', error);
|
||
alert('导出失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 打开编辑器
|
||
function openEditor() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可编辑的内容');
|
||
return;
|
||
}
|
||
|
||
currentEditingText = lastGeneratedResult.generated_text;
|
||
editorTextarea.value = currentEditingText;
|
||
editorModal.classList.remove('hidden');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
// 保存编辑
|
||
function saveEdit() {
|
||
const editedText = editorTextarea.value;
|
||
lastGeneratedResult.generated_text = editedText;
|
||
|
||
// 更新显示内容
|
||
const answerParagraphs = answerContent.querySelectorAll('p');
|
||
for (let p of answerParagraphs) {
|
||
if (p.innerHTML.includes('回答:')) {
|
||
const nextP = p.nextElementSibling;
|
||
if (nextP && nextP.tagName === 'P' && !nextP.classList.contains('warning')) {
|
||
nextP.innerHTML = editedText.replace(/\n/g, '<br>');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 关闭编辑器
|
||
editorModal.classList.add('hidden');
|
||
document.body.style.overflow = 'auto';
|
||
alert('编辑已保存');
|
||
}
|
||
|
||
// 取消编辑
|
||
function cancelEdit() {
|
||
editorTextarea.value = currentEditingText;
|
||
editorModal.classList.add('hidden');
|
||
document.body.style.overflow = 'auto';
|
||
}
|
||
|
||
// 显示消息
|
||
function showMessage(message, type) {
|
||
const globalAlert = document.getElementById('globalAlert');
|
||
globalAlert.textContent = message;
|
||
globalAlert.className = 'alert ' + type;
|
||
globalAlert.classList.remove('hidden');
|
||
|
||
// 3秒后自动隐藏消息
|
||
setTimeout(() => {
|
||
globalAlert.classList.add('hidden');
|
||
}, 3000);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
throw new Error(result.detail || '生成失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('生成错误:', error);
|
||
generateResultContent.innerHTML = `<p class="error">生成失败: ${error.message}</p>`;
|
||
generateResult.classList.remove('hidden');
|
||
|
||
// 重新启用按钮
|
||
generateBtn.disabled = false;
|
||
generateAsyncBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 检查生成任务状态
|
||
async function checkGenerateTaskStatus() {
|
||
if (!generateTaskId) {
|
||
alert('没有正在运行的生成任务');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/task_status/${generateTaskId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.state === 'PENDING') {
|
||
generateTaskStatusSpan.textContent = '任务等待中...';
|
||
} else if (result.state === 'PROGRESS') {
|
||
generateTaskStatusSpan.textContent = `${result.status} (${result.percent}%)`;
|
||
|
||
// 更新进度条
|
||
generateProgress.classList.remove('hidden');
|
||
generateProgressBar.style.width = `${result.percent}%`;
|
||
generateProgressBar.textContent = `${result.percent}%`;
|
||
} else if (result.state === 'SUCCESS') {
|
||
generateTaskStatusSpan.textContent = '任务完成';
|
||
|
||
// 显示结果
|
||
const taskResult = result.result;
|
||
|
||
// 保存结果用于导出
|
||
lastGeneratedResult = {
|
||
query: taskResult.query,
|
||
generated_text: taskResult.generated_text
|
||
};
|
||
|
||
let resultHTML = `
|
||
<h3 class="success">生成结果</h3>
|
||
<p><strong>问题:</strong> ${taskResult.query}</p>
|
||
<p><strong>生成文案:</strong></p>
|
||
<p>${taskResult.generated_text.replace(/\n/g, '<br>')}</p>
|
||
`;
|
||
|
||
// 显示幻觉警告信息
|
||
if (taskResult.hallucination_warnings) {
|
||
const warnings = taskResult.hallucination_warnings;
|
||
let warningHTML = '';
|
||
|
||
if (warnings.keywords && warnings.keywords.length > 0) {
|
||
warningHTML += `<p><strong>幻觉关键词:</strong> ${warnings.keywords.join(', ')}</p>`;
|
||
}
|
||
|
||
if (warnings.entities && warnings.entities.length > 0) {
|
||
warningHTML += `<p><strong>幻觉实体:</strong> ${warnings.entities.join(', ')}</p>`;
|
||
}
|
||
|
||
if (warningHTML) {
|
||
resultHTML += `
|
||
<div class="hallucination-warning">
|
||
<p class="warning">⚠️ 检测到可能的幻觉内容:</p>
|
||
${warningHTML}
|
||
<p class="warning">请核对原文确认表述准确性</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
generateResultContent.innerHTML = resultHTML;
|
||
generateResult.classList.remove('hidden');
|
||
exportBtn.classList.remove('hidden');
|
||
editBtn.classList.remove('hidden');
|
||
scoreBtn.classList.remove('hidden');
|
||
|
||
// 隐藏进度条
|
||
generateProgress.classList.add('hidden');
|
||
|
||
// 标记步骤4为完成
|
||
steps[3].classList.add('completed');
|
||
} else {
|
||
generateTaskStatusSpan.textContent = `任务失败: ${result.error}`;
|
||
|
||
// 显示错误
|
||
generateResultContent.innerHTML = `<p class="error">任务失败: ${result.error}</p>`;
|
||
generateResult.classList.remove('hidden');
|
||
|
||
// 隐藏进度条
|
||
generateProgress.classList.add('hidden');
|
||
}
|
||
} catch (error) {
|
||
console.error('检查任务状态错误:', error);
|
||
generateTaskStatusSpan.textContent = '检查状态失败';
|
||
}
|
||
}
|
||
|
||
// 生成评分
|
||
async function scoreGeneration() {
|
||
if (!lastGeneratedResult || !lastContext || !lastQuery) {
|
||
alert('没有可评分的内容');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建FormData
|
||
const formData = new FormData();
|
||
formData.append('generated_text', lastGeneratedResult.generated_text);
|
||
formData.append('context', lastContext);
|
||
formData.append('query', lastQuery);
|
||
|
||
// 发送评分请求
|
||
const response = await fetch('/score_generation', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const scoreResultData = await response.json();
|
||
|
||
if (response.ok) {
|
||
// 显示评分结果
|
||
displayScoreResult(scoreResultData);
|
||
} else {
|
||
throw new Error(scoreResultData.detail || '评分失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('评分错误:', error);
|
||
scoreResult.innerHTML = `<p class="error">评分失败: ${error.message}</p>`;
|
||
scoreResult.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// 显示评分结果
|
||
function displayScoreResult(scoreData) {
|
||
let scoreHTML = `
|
||
<h3 class="success">生成评分结果</h3>
|
||
<p><strong>总分:</strong> ${scoreData.total_score || scoreData.score}/100</p>
|
||
`;
|
||
|
||
if (scoreData.dimensions) {
|
||
scoreHTML += `
|
||
<p><strong>各维度得分:</strong></p>
|
||
<ul>
|
||
<li>相关性: ${scoreData.dimensions.relevance}/30</li>
|
||
<li>准确性: ${scoreData.dimensions.accuracy}/30</li>
|
||
<li>完整性: ${scoreData.dimensions.completeness}/20</li>
|
||
<li>流畅性: ${scoreData.dimensions.fluency}/20</li>
|
||
</ul>
|
||
`;
|
||
}
|
||
|
||
if (scoreData.feedback) {
|
||
scoreHTML += `<p><strong>反馈意见:</strong> ${scoreData.feedback}</p>`;
|
||
}
|
||
|
||
if (scoreData.suggestions) {
|
||
scoreHTML += `<p><strong>改进建议:</strong> ${scoreData.suggestions}</p>`;
|
||
}
|
||
|
||
scoreResult.innerHTML = scoreHTML;
|
||
scoreResult.classList.remove('hidden');
|
||
}
|
||
|
||
// 导出为Markdown
|
||
async function exportToMarkdown() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可导出的内容');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建FormData
|
||
const formData = new FormData();
|
||
formData.append('query', lastGeneratedResult.query);
|
||
formData.append('generated_text', lastGeneratedResult.generated_text);
|
||
|
||
// 发送导出请求
|
||
const response = await fetch('/export_markdown', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = response.headers.get('content-disposition')
|
||
.split('filename=')[1]
|
||
.replace(/"/g, '');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
alert('导出成功!');
|
||
} else {
|
||
throw new Error('导出失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出错误:', error);
|
||
alert('导出失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 导出为DOCX
|
||
async function exportToDocx() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可导出的内容');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建FormData
|
||
const formData = new FormData();
|
||
formData.append('query', lastGeneratedResult.query);
|
||
formData.append('generated_text', lastGeneratedResult.generated_text);
|
||
|
||
// 发送导出请求
|
||
const response = await fetch('/export_docx', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = response.headers.get('content-disposition')
|
||
.split('filename=')[1]
|
||
.replace(/"/g, '');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
|
||
alert('DOCX导出成功!');
|
||
} else {
|
||
throw new Error('导出失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出错误:', error);
|
||
alert('导出失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 打开编辑器
|
||
function openEditor() {
|
||
if (!lastGeneratedResult) {
|
||
alert('没有可编辑的内容');
|
||
return;
|
||
}
|
||
|
||
currentEditingText = lastGeneratedResult.generated_text;
|
||
editorTextarea.value = currentEditingText;
|
||
editorModal.classList.remove('hidden');
|
||
editorModal.style.display = 'flex'; // 确保显示
|
||
editorModal.style.visibility = 'visible'; // 确保可见
|
||
// 防止背景滚动
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
// 保存编辑
|
||
function saveEdit() {
|
||
const editedText = editorTextarea.value;
|
||
lastGeneratedResult.generated_text = editedText;
|
||
|
||
// 更新显示内容
|
||
const paragraphs = generateResultContent.querySelectorAll('p');
|
||
for (let p of paragraphs) {
|
||
if (p.innerHTML.includes('生成文案:')) {
|
||
// 找到生成文案的段落并更新内容
|
||
const nextP = p.nextElementSibling;
|
||
if (nextP && nextP.tagName === 'P') {
|
||
nextP.innerHTML = editedText.replace(/\n/g, '<br>');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 关闭编辑器
|
||
editorModal.classList.add('hidden');
|
||
editorModal.style.display = 'none'; // 确保隐藏
|
||
editorModal.style.visibility = 'hidden'; // 确保隐藏
|
||
// 恢复背景滚动
|
||
document.body.style.overflow = 'auto';
|
||
editorTextarea.blur(); // 清除焦点
|
||
alert('编辑已保存');
|
||
}
|
||
|
||
// 取消编辑
|
||
function cancelEdit() {
|
||
// 恢复原始文本
|
||
editorTextarea.value = currentEditingText;
|
||
// 关闭编辑器
|
||
editorModal.classList.add('hidden');
|
||
editorModal.style.display = 'none'; // 确保隐藏
|
||
// 恢复背景滚动
|
||
document.body.style.overflow = 'auto';
|
||
// 清除焦点
|
||
editorTextarea.blur();
|
||
// 防止事件冒泡
|
||
if (event) {
|
||
event.stopPropagation();
|
||
}
|
||
// 确保完全隐藏
|
||
setTimeout(() => {
|
||
editorModal.style.visibility = 'hidden';
|
||
}, 10);
|
||
}
|
||
|
||
// 定期检查任务状态
|
||
setInterval(() => {
|
||
if (uploadTaskId) {
|
||
checkUploadTaskStatus();
|
||
}
|
||
if (generateTaskId) {
|
||
checkGenerateTaskStatus();
|
||
}
|
||
}, 3000); // 每3秒检查一次
|
||
</script>
|
||
</body>
|
||
</html> |