mirror of
https://github.com/HKUDS/LightRAG.git
synced 2025-07-03 15:10:03 +00:00
504 lines
16 KiB
JavaScript
504 lines
16 KiB
JavaScript
// 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 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 = `
|
||
<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);
|