Files
agentic/frontend/js/utils.js
T
2026-06-10 08:20:27 +02:00

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
// 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(/^&gt; (.+)$/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" });
}
}