504 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lightrag.js
const API_BASE = 'http://localhost:9621'; // 根据实际API地址修改
// init
function initializeApp() {
setupFileUpload();
setupQueryHandler();
setupSectionObserver();
updateFileList();
// 文本输入框实时字数统计 textarea count
const textArea = document.getElementById('textContent');
if (textArea) {
const charCount = document.createElement('div');
charCount.className = 'char-count';
textArea.parentNode.appendChild(charCount);
textArea.addEventListener('input', () => {
const count = textArea.value.length;
charCount.textContent = `input ${count} character`;
charCount.style.color = count > 10000 ? '#ef4444' : 'var (--text-secondary)'
});
}
}
// 通用请求方法 api request
async function apiRequest(endpoint, method = 'GET', body = null) {
const options = {
method,
headers: {
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(`${API_BASE}${endpoint}`, options);
if (!response.ok) {
throw new Error(`request failed: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('API REQUEST ERROR:', error);
showToast(error.message, 'error');
throw error;
}
}
async function handleTextUpload() {
const description = document.getElementById('textDescription').value;
const content = document.getElementById('textContent').value.trim();
const statusDiv = document.getElementById('textUploadStatus');
// 清空状态提示 clear status tip
statusDiv.className = 'status-indicator';
statusDiv.textContent = '';
// 输入验证 input valid
if (!content) {
showStatus('error', 'TEXT CONTENT NOT NULL', statusDiv);
return;
}
try {
showStatus('loading', 'UPLOADING...', statusDiv);
//
const payload = {
text: content,
...(description && {description})
};
const response = await fetch(`${API_BASE}/documents/text`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || '上传失败');
}
const result = await response.json();
showStatus('success', `${result.message} (文档数: ${result.document_count})`, statusDiv);
document.getElementById('textContent').value = '';
// 更新文件列表 update file list
updateFileList();
} catch (error) {
showStatus('error', `❌ ERROR: ${error.message}`, statusDiv);
console.error('FILE UPLOAD FAILED:', error);
}
}
function showStatus(type, message, container) {
container.textContent = message;
container.className = `status-indicator ${type}`;
// 自动清除成功状态 auto clear success status
if (type === 'success') {
setTimeout(() => {
container.textContent = '';
container.className = 'status-indicator';
}, 5000);
}
}
// 文件上传处理 upload file
function setupFileUpload() {
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
// 拖放事件处理 Drag and drop event handling
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('active');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('active');
});
dropzone.addEventListener('drop', async (e) => {
e.preventDefault();
dropzone.classList.remove('active');
await handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', async (e) => {
await handleFiles(e.target.files);
});
}
async function handleFiles(files) {
const formData = new FormData();
for (const file of files) {
formData.append('file', file);
}
const statusDiv = document.getElementById('fileUploadStatus');
statusDiv.className = 'status-indicator';
statusDiv.textContent = '';
try {
showStatus('loading', 'UPLOADING...', statusDiv);
const response = await fetch(`${API_BASE}/documents/upload`, {
method: 'POST',
body: formData
});
const result = await response.json();
showStatus('success', `${result.message} `, statusDiv);
updateFileList();
} catch (error) {
showToast(error.message, 'error');
}
}
async function updateFileList() {
const fileList = document.querySelector('.file-list');
try {
const status = await apiRequest('/health');
fileList.innerHTML = `
<div>INDEXED FILE: ${status.indexed_files_count}</div>
`;
} catch (error) {
fileList.innerHTML = 'UNABLE TO OBTAIN FILE LIST';
}
}
// 智能检索处理 Intelligent retrieval processing
function setupQueryHandler() {
document.querySelector('#query .btn-primary').addEventListener('click', handleQuery);
}
async function handleQuery() {
const queryInput = document.querySelector('#query textarea');
const modeSelect = document.querySelector('#query select');
const streamCheckbox = document.querySelector('#query input[type="checkbox"]');
const resultsDiv = document.querySelector('#query .results');
const payload = {
query: queryInput.value,
mode: modeSelect.value,
stream: streamCheckbox.checked
};
resultsDiv.innerHTML = '<div class="loading">SEARCHING...</div>';
try {
if (payload.stream) {
await handleStreamingQuery(payload, resultsDiv);
} else {
const result = await apiRequest('/query', 'POST', payload);
resultsDiv.innerHTML = `<div class="result">${result.response}</div>`;
}
} catch (error) {
resultsDiv.innerHTML = `<div class="error">SEARCH FAILED: ${error.message}</div>`;
}
}
// 处理流式响应 handle stream api
async function handleStreamingQuery(payload, resultsDiv) {
try {
const response = await fetch(`${API_BASE}/query/stream`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const contentType = response.headers.get('Content-Type') || '';
const validTypes = ['application/x-ndjson', 'application/json'];
if (!validTypes.some(t => contentType.includes(t))) {
throw new Error(`INVALID CONTENT TYPE: ${contentType}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
resultsDiv.innerHTML = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
// 按换行符分割NDJSON格式要求 Split by line break (NDJSON format requirement)
let lineEndIndex;
while ((lineEndIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
if (!line) continue;
try {
const data = JSON.parse(line);
if (data.response) {
resultsDiv.innerHTML += data.response;
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
if (data.error) {
resultsDiv.innerHTML += `<div class="error">${data.error}</div>`;
}
} catch (error) {
console.error('JSON PARSING FAILED:', {
error,
rawLine: line,
bufferRemaining: buffer
});
}
}
}
// 处理剩余数据 Process remaining data
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
if (data.response) {
resultsDiv.innerHTML += data.response;
}
} catch (error) {
console.error('TAIL DATA PARSING FAILED:', error);
}
}
} catch (error) {
resultsDiv.innerHTML = `<div class="error">REQUEST FAILED: ${error.message}</div>`;
}
}
// 知识问答处理 Knowledge Q&A Processing
function setupChatHandler() {
const sendButton = document.querySelector('#chat button');
const chatInput = document.querySelector('#chat input');
sendButton.addEventListener('click', () => handleChat(chatInput));
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleChat(chatInput);
});
}
async function handleChat(chatInput) {
const chatHistory = document.querySelector('#chat .chat-history');
const userMessage = document.createElement('div');
userMessage.className = 'message user';
userMessage.textContent = chatInput.value;
chatHistory.appendChild(userMessage);
const botMessage = document.createElement('div');
botMessage.className = 'message bot loading';
botMessage.textContent = 'THINKING...';
chatHistory.appendChild(botMessage);
chatHistory.scrollTop = chatHistory.scrollHeight;
try {
const response = await fetch(`${API_BASE}/api/chat`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
messages: [{role: "user", content: chatInput.value}],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
botMessage.classList.remove('loading');
botMessage.textContent = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const data = JSON.parse(chunk);
botMessage.textContent += data.message?.content || '';
chatHistory.scrollTop = chatHistory.scrollHeight;
}
} catch (error) {
botMessage.textContent = `ERROR: ${error.message}`;
botMessage.classList.add('error');
}
chatInput.value = '';
}
// 系统状态更新 system status update
async function updateSystemStatus() {
const statusElements = {
health: document.getElementById('healthStatus'),
storageProgress: document.getElementById('storageProgress'),
indexedFiles: document.getElementById('indexedFiles'),
storageUsage: document.getElementById('storageUsage'),
llmModel: document.getElementById('llmModel'),
embedModel: document.getElementById('embedModel'),
maxTokens: document.getElementById('maxTokens'),
workingDir: document.getElementById('workingDir'),
inputDir: document.getElementById('inputDir'),
kv_storage: document.getElementById("kv_storage"),
doc_status_storage: document.getElementById("doc_status_storage"),
graph_storage: document.getElementById("graph_storage"),
vector_storage: document.getElementById("vector_storage")
};
try {
const status = await apiRequest('/health');
// 健康状态 heath status
statusElements.health.className = 'status-badge';
statusElements.health.textContent = status.status === 'healthy' ?
'✅ Healthy operation in progress' : '⚠️ Service exception';
// 存储状态(示例逻辑,可根据实际需求修改) kv status
const progressValue = Math.min(Math.round((status.indexed_files_count / 1000) * 100), 100);
statusElements.storageProgress.value = progressValue;
statusElements.indexedFiles.textContent = `INDEXED FILES${status.indexed_files_count}`;
statusElements.storageUsage.textContent = `USE PERCENT${progressValue}%`;
// 模型配置 model state
statusElements.llmModel.textContent = `${status.configuration.llm_model} (${status.configuration.llm_binding})`;
statusElements.embedModel.textContent = `${status.configuration.embedding_model} (${status.configuration.embedding_binding})`;
statusElements.maxTokens.textContent = status.configuration.max_tokens.toLocaleString();
// 目录信息 dir msg
statusElements.workingDir.textContent = status.working_directory;
statusElements.inputDir.textContent = status.input_directory;
//存储信息 stack msg
statusElements.kv_storage.textContent = status.configuration.kv_storage;
statusElements.doc_status_storage.textContent = status.configuration.doc_status_storage;
statusElements.graph_storage.textContent = status.configuration.graph_storage;
statusElements.vector_storage.textContent = status.configuration.vector_storage
} catch (error) {
statusElements.health.className = 'status-badge error';
statusElements.health.textContent = '❌GET STATUS FAILED';
statusElements.storageProgress.value = 0;
statusElements.indexedFiles.textContent = 'INDEXED FILESGET FAILED';
console.error('STATUS UPDATE FAILED:', error);
}
}
// 区域切换监听 Area switching monitoring
function setupSectionObserver() {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'style') {
const isVisible = mutation.target.style.display !== 'none';
if (isVisible && mutation.target.id === 'status') {
updateSystemStatus();
}
}
});
});
document.querySelectorAll('.card').forEach(section => {
observer.observe(section, {attributes: true});
});
}
// 显示提示信息 Display prompt information
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 动态加载标签列表 Dynamically load tag list
async function loadLabels() {
try {
const response = await fetch('/graph/label/list');
const labels = await response.json();
renderLabels(labels);
} catch (error) {
console.error('DYNAMICALLY LOAD TAG LIST FAILED:', error);
}
}
async function loadGraph(label) {
try {
// 渲染标签列表 render label list
openGraphModal(label)
} catch (error) {
console.error('LOADING LABEL FAILED:', error);
const labelList = document.getElementById("label-list");
labelList.innerHTML = `
<div class="error-message">
LOADING ERROR: ${error.message}
</div>
`;
}
}
// 渲染标签列表 render graph label list
function renderLabels(labels) {
const container = document.getElementById('label-list');
container.innerHTML = labels.map(label => `
<div class="label-item">
<span style="font-weight: 500; color: var(--text-primary);">
${label}
</span>
<div class="label-actions">
<button class="btn btn-primary"
onclick="handleLabelAction('${label}')">
📋 graph
</button>
</div>
</div>
`).join('');
}
function handleLabelAction(label) {
loadGraph(label)
}
function refreshLabels() {
showToast('LOADING GRAPH LABELS...', 'info');
loadLabels();
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
document.addEventListener('DOMContentLoaded', initializeApp);