// 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 = `
INDEXED FILE: ${status.indexed_files_count}
`;
} 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 = 'SEARCHING...
';
try {
if (payload.stream) {
await handleStreamingQuery(payload, resultsDiv);
} else {
const result = await apiRequest('/query', 'POST', payload);
resultsDiv.innerHTML = `${result.response}
`;
}
} catch (error) {
resultsDiv.innerHTML = `SEARCH FAILED: ${error.message}
`;
}
}
// 处理流式响应 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 += `${data.error}
`;
}
} 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 = `REQUEST FAILED: ${error.message}
`;
}
}
// 知识问答处理 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 FILES:GET 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 = `
LOADING ERROR: ${error.message}
`;
}
}
// 渲染标签列表 render graph label list
function renderLabels(labels) {
const container = document.getElementById('label-list');
container.innerHTML = labels.map(label => `
`).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);