157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
// App state store
|
|
const STORE = {
|
|
currentDocId: null,
|
|
currentSessionId: null,
|
|
currentPage: 0,
|
|
totalPages: 0,
|
|
documents: [],
|
|
sessions: [],
|
|
polygons: [],
|
|
viewerDoc: null,
|
|
chunkCache: {},
|
|
chatMessages: [],
|
|
};
|
|
|
|
// API helpers
|
|
const API = {
|
|
async get(url) {
|
|
const r = await fetch(url);
|
|
if (!r.ok) throw new Error(`API ${r.status}: ${r.statusText}`);
|
|
return r.json();
|
|
},
|
|
async post(url, body) {
|
|
const r = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!r.ok) throw new Error(`API ${r.status}: ${r.statusText}`);
|
|
return r.json();
|
|
},
|
|
};
|
|
|
|
// DOM helpers
|
|
const $ = (sel) => document.querySelector(sel);
|
|
const $$ = (sel) => document.querySelectorAll(sel);
|
|
|
|
// Render markdown-style content
|
|
function renderContent(text) {
|
|
if (!text) return "<p class='typing'>No content</p>";
|
|
|
|
let html = text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
|
|
// Headers
|
|
html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
|
html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
|
html = html.replace(/^# (.+)$/gm, '<h3 class="research-answer" style="color:var(--accent-blue)">$1</h3>');
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
|
|
// Code blocks
|
|
html = html.replace(/```([\s\S]*?)```/g, '<div class="code-block" style="background:var(--bg-tertiary);padding:10px;border-radius:6px;font-family:monospace;font-size:12px;margin:8px 0;overflow-x:auto">$1</div>');
|
|
|
|
// Inline code
|
|
html = html.replace(/`(.+?)`/g, '<code style="background:var(--bg-tertiary);padding:2px 5px;border-radius:3px;font-size:12px">$1</code>');
|
|
|
|
// Unordered lists
|
|
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
|
|
|
|
// Ordered lists
|
|
html = html.replace(/^\d+\. (.+)$/gm, '<li><strong>$1</strong></li>');
|
|
|
|
// Horizontal rules
|
|
html = html.replace(/^---$/gm, '<hr style="border:none;border-top:1px solid var(--border-color);margin:12px 0">');
|
|
|
|
// Blockquotes
|
|
html = html.replace(/^> (.+)$/gm, '<div class="block" style="border-left:3px solid var(--accent-purple);padding:8px 12px;margin:6px 0;font-style:italic;color:var(--text-secondary)">$1</div>');
|
|
|
|
// Tables
|
|
html = html.replace(/^\|(.+)\|$/gm, (match, content) => {
|
|
const cells = content.split('|').map(c => c.trim());
|
|
if (cells.every(c => /^[-:]+$/.test(c))) {
|
|
return '<!--table-sep-->';
|
|
}
|
|
if (cells.every(c => c.length < 30 && !c.includes(' '))) {
|
|
return '<tr>' + cells.map(c => `<td>${c}</td>`).join('') + '</tr>';
|
|
}
|
|
return `<tr><td colspan="${cells.length}">${cells.join('</td><td>')}</td></tr>`;
|
|
});
|
|
|
|
// Wrap rows in table
|
|
let inTable = false;
|
|
const lines = html.split('\n');
|
|
let result = [];
|
|
let tableBuffer = [];
|
|
|
|
for (let line of lines) {
|
|
if (line.includes('<!--table-sep-->')) {
|
|
if (tableBuffer.length > 0) {
|
|
result.push('<table>' + tableBuffer.join('') + '</table>');
|
|
tableBuffer = [];
|
|
}
|
|
continue;
|
|
}
|
|
if (line.startsWith('<tr>')) {
|
|
tableBuffer.push(line);
|
|
} else {
|
|
if (tableBuffer.length > 0) {
|
|
result.push('<table>' + tableBuffer.join('') + '</table>');
|
|
tableBuffer = [];
|
|
}
|
|
result.push(line);
|
|
}
|
|
}
|
|
if (tableBuffer.length > 0) {
|
|
result.push('<table>' + tableBuffer.join('') + '</table>');
|
|
}
|
|
html = result.join('\n');
|
|
|
|
// Paragraphs
|
|
html = html.replace(/^(?!<[huldtbr]|<!--)/gm, '<p>$&</p>');
|
|
|
|
return html;
|
|
}
|
|
|
|
function showSpinner(el) {
|
|
el.innerHTML = '<div class="spinner"></div> <span class="typing">Processing...</span>';
|
|
}
|
|
|
|
function showToast(msg, type = "info") {
|
|
const toast = document.createElement("div");
|
|
toast.textContent = msg;
|
|
toast.style.cssText = `
|
|
position: fixed; bottom: 20px; right: 20px; padding: 10px 16px;
|
|
background: ${type === "error" ? "var(--accent-red)" : "var(--accent-blue)"};
|
|
color: #fff; border-radius: 6px; font-size: 13px; z-index: 999;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); animation: fadein 0.3s;
|
|
`;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3000);
|
|
}
|
|
|
|
// Load initial state
|
|
async function loadInitialState() {
|
|
try {
|
|
const docs = await API.get("/api/documents");
|
|
STORE.documents = docs.documents || [];
|
|
renderDocList();
|
|
|
|
const health = await API.get("/health");
|
|
updateHealth(health);
|
|
|
|
const models = await API.get("/api/ollama/models");
|
|
renderModels(models);
|
|
} catch (e) {
|
|
console.warn("Initial load failed:", e);
|
|
updateHealth({ status: "err" });
|
|
}
|
|
}
|