");
inOrderedList = true;
}
const nestedClass = orderedItem[1].length >= 2 ? ' class="nested"' : "";
output.push(`- ${inlineMarkdown(orderedItem[2])}
`);
continue;
}
closeLists();
if (/^>\s?/.test(trimmed)) {
output.push(`${inlineMarkdown(trimmed.replace(/^>\s?/, ""))}
`);
} else if (/^---+$/.test(trimmed)) {
output.push("
");
} else if (/^#{1,6}\s+/.test(trimmed)) {
const level = Math.min(4, trimmed.match(/^#+/)[0].length + 2);
output.push(`${inlineMarkdown(trimmed.replace(/^#{1,6}\s+/, ""))}`);
} else if (trimmed) {
output.push(`${inlineMarkdown(line)}
`);
} else {
output.push("
");
}
}
if (inCode) flushCode();
closeLists();
return output.join("");
}
function inlineMarkdown(text) {
return escapeHtml(text)
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '$1');
}
function isTableRow(line) {
const trimmed = line.trim();
return trimmed.includes("|") && /^\|?.+\|.+\|?$/.test(trimmed);
}
function isTableDivider(line) {
return splitTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
}
function splitTableRow(line) {
return line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim());
}
function tableAlignment(cell) {
const trimmed = cell.trim();
if (trimmed.startsWith(":") && trimmed.endsWith(":")) return "center";
if (trimmed.endsWith(":")) return "right";
return "left";
}
function renderTable(headers, aligns, rows) {
const head = headers
.map((cell, index) => `${inlineMarkdown(cell)} | `)
.join("");
const body = rows
.map((row) => {
const cells = headers
.map((_, index) => `${inlineMarkdown(row[index] || "")} | `)
.join("");
return `${cells}
`;
})
.join("");
return ``;
}
function escapeHtml(text) {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function formatMetrics(event) {
const read = formatTokenMetric(event.reading_tokens, event.reading_tokens_per_second);
const wrote = formatTokenMetric(event.writing_tokens, event.writing_tokens_per_second);
return [read && `read ${read}`, wrote && `wrote ${wrote}`].filter(Boolean).join(" | ");
}
function formatTokenMetric(tokens, speed) {
if (!tokens) return "";
const speedText = typeof speed === "number" ? ` @ ${speed.toFixed(1)}/s` : "";
return `${tokens} tok${speedText}`;
}
function setWarning(text) {
warningEl.hidden = !text;
warningEl.textContent = text || "";
}
function fetchErrorMessage(error) {
if (error instanceof TypeError && /fetch/i.test(error.message)) {
return "TraderAI backend is not reachable. Close this app window and launch TraderAI.exe again.";
}
return error.message;
}
const configFieldIds = {
uex_base_url: "config-uex-base-url",
uex_secret_key: "config-uex-secret-key",
uex_bearer_token: "config-uex-bearer-token",
traderai_user_name: "config-traderai-user-name",
traderai_memory_path: "config-traderai-memory-path",
uex_notification_poll_seconds: "config-uex-notification-poll-seconds",
require_write_approval: "config-require-write-approval",
};
const ollamaFieldIds = {
ollama_base_url: "ollama-base-url",
ollama_model: "ollama-model",
ollama_num_ctx: "ollama-num-ctx",
};
async function refreshConfig() {
try {
const response = await fetch("/api/config");
const config = await response.json();
renderConfig(config);
} catch (error) {
configStatusEl.textContent = `Config load failed: ${fetchErrorMessage(error)}`;
}
}
function renderConfig(config) {
const values = config.values || {};
const secretsConfigured = config.secrets_configured || {};
for (const [key, id] of Object.entries(configFieldIds)) {
const field = document.getElementById(id);
if (!field) continue;
if (field.type === "checkbox") {
field.checked = Boolean(values[key]);
} else if (field.type === "password") {
field.value = "";
field.placeholder = secretsConfigured[key] ? "Configured" : "";
} else {
field.value = values[key] ?? "";
}
}
for (const [key, id] of Object.entries(ollamaFieldIds)) {
const field = document.getElementById(id);
if (!field) continue;
field.value = values[key] ?? "";
}
configPathsEl.textContent = `App data: ${config.app_data_dir}\nConfig: ${config.config_path}\nLog: ${config.log_path}\nEdge profile: ${config.edge_profile_dir}`;
configStatusEl.textContent = "";
}
async function saveConfig(event) {
event.preventDefault();
const values = {};
for (const [key, id] of Object.entries(configFieldIds)) {
const field = document.getElementById(id);
if (!field) continue;
values[key] = field.type === "checkbox" ? field.checked : field.value;
}
configStatusEl.textContent = "Saving";
try {
const response = await fetch("/api/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ values }),
});
const result = await response.json();
renderConfig(result);
configStatusEl.textContent = result.message || "Saved";
addMessage("assistant", "Config saved. Restart TraderAI for the new settings to fully apply.");
} catch (error) {
configStatusEl.textContent = `Config save failed: ${fetchErrorMessage(error)}`;
}
}
async function saveOllamaConfig(event) {
event.preventDefault();
const values = {};
for (const [key, id] of Object.entries(ollamaFieldIds)) {
const field = document.getElementById(id);
if (!field) continue;
values[key] = field.value;
}
setOllamaMessage("Saving Ollama config");
try {
const response = await fetch("/api/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ values }),
});
const result = await response.json();
renderConfig(result);
setOllamaMessage(result.message || "Saved");
await refreshOllamaStatus();
} catch (error) {
setOllamaMessage(`Ollama config save failed: ${fetchErrorMessage(error)}`);
}
}
async function refreshOllamaStatus() {
if (!ollamaStatusEl) return;
ollamaStatusEl.textContent = "Checking Ollama";
try {
const response = await fetch("/api/ollama/status");
const status = await response.json();
renderOllamaStatus(status);
} catch (error) {
ollamaStatusEl.textContent = `Ollama status failed: ${error.message}`;
}
}
function renderOllamaStatus(status) {
if (!ollamaStatusEl) return;
const models = status.models?.length ? status.models.join(", ") : "None detected";
const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning";
ollamaStatusEl.innerHTML = `
${escapeHtml(status.message || "Unknown")}
${ollamaStatusItem("Installed", status.installed ? "Yes" : "No")}
${ollamaStatusItem("Running", status.running ? "Yes" : "No")}
${ollamaStatusItem("Model", status.configured_model || "")}
${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")}
${ollamaStatusItem("URL", status.base_url || "")}
${ollamaStatusItem("Auto Install", status.can_auto_install ? "Available" : "Unavailable")}
${ollamaStatusItem("Installed Models", models)}
${status.detail ? ollamaStatusItem("Detail", status.detail) : ""}
`;
if (ollamaInstallButton) ollamaInstallButton.disabled = Boolean(status.installed);
if (ollamaLaunchButton) ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
}
function ollamaStatusItem(label, value) {
return `${escapeHtml(label)}${escapeHtml(String(value ?? ""))}
`;
}
function setOllamaMessage(message) {
if (ollamaMessageEl) ollamaMessageEl.textContent = message || "";
}
async function postOllamaAction(endpoint, options = {}) {
setOllamaMessage("Working");
try {
const response = await fetch(endpoint, {
method: "POST",
headers: options.body ? { "Content-Type": "application/json" } : undefined,
body: options.body ? JSON.stringify(options.body) : undefined,
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
setOllamaMessage(result.message || "Done");
await refreshOllamaStatus();
} catch (error) {
setOllamaMessage(error.message);
}
}
function configuredOllamaModel() {
return document.getElementById("ollama-model")?.value || "";
}
async function checkForUpdate() {
if (!updateStatusEl) return;
updateStatusEl.textContent = "Checking releases";
try {
const response = await fetch("/api/update/check");
const result = await response.json();
latestUpdate = result;
renderUpdateStatus(result);
} catch (error) {
updateStatusEl.textContent = `Update check failed: ${error.message}`;
if (updateInstallButton) updateInstallButton.disabled = true;
}
}
function renderUpdateStatus(update) {
if (!updateStatusEl) return;
const lines = [
`Current: ${update.current_version || "unknown"}`,
`Latest: ${update.latest_version || "unknown"}`,
update.message || "",
].filter(Boolean);
if (update.available && !update.asset_download_url) {
lines.push("The release needs a TraderAI.exe attachment before the app can self-update.");
}
if (update.available && update.asset_download_url && !update.packaged) {
lines.push("Self-update runs from the packaged desktop exe.");
}
updateStatusEl.textContent = lines.join("\n");
if (updateInstallButton) {
updateInstallButton.disabled = !update.available || !update.asset_download_url || !update.packaged;
}
}
async function installUpdate() {
if (!updateStatusEl) return;
updateStatusEl.textContent = "Downloading update";
try {
const response = await fetch("/api/update/install", { method: "POST" });
const result = await response.json();
latestUpdate = result;
renderUpdateStatus(result);
} catch (error) {
updateStatusEl.textContent = `Update failed: ${error.message}`;
}
}
function openReleasesPage() {
const url = latestUpdate?.release_url || "https://git.hudsonriggs.systems/LambdaBankingConglomerate/TraderAI/releases";
window.open(url, "_blank", "noreferrer");
}
function toggleSidebarPanel(panelName) {
const panels = {
settings: { panel: settingsPanel, button: settingsToggle },
memory: { panel: memoryPanel, button: memoryToggle },
ollama: { panel: ollamaPanel, button: ollamaToggle },
};
const target = panels[panelName];
if (!target?.panel || !target?.button) return;
const shouldOpen = target.panel.hidden;
for (const item of Object.values(panels)) {
if (!item.panel || !item.button) continue;
item.panel.hidden = true;
item.button.classList.remove("active");
item.button.setAttribute("aria-expanded", "false");
}
if (shouldOpen) {
target.panel.hidden = false;
target.button.classList.add("active");
target.button.setAttribute("aria-expanded", "true");
if (panelName === "settings") {
refreshConfig();
checkForUpdate();
}
if (panelName === "memory") refreshMemory();
if (panelName === "ollama") {
refreshConfig();
refreshOllamaStatus();
}
}
}
async function checkHealth() {
try {
const response = await fetch("/api/health");
const result = await response.json();
const health = result.ollama || {};
ollamaOnline = Boolean(health.online);
if (!ollamaOnline) {
statusEl.textContent = "Offline";
setWarning(health.message || "Ollama is offline. Start Ollama before chatting.");
return false;
}
if (health.model_available === false) {
setWarning(`Ollama is online, but model "${health.model}" is not pulled. Run: ollama pull ${health.model}`);
} else {
setWarning("");
}
statusEl.textContent = "Ready";
return true;
} catch (error) {
ollamaOnline = false;
statusEl.textContent = "Offline";
setWarning(`Could not check Ollama health: ${error.message}`);
return false;
}
}
function renderPending(actions) {
pendingEl.innerHTML = "";
if (!actions.length) {
pendingEl.className = "pending-empty";
pendingEl.textContent = "No pending actions";
return;
}
pendingEl.className = "";
for (const action of actions) {
const card = document.createElement("div");
card.className = "pending-card";
const title = document.createElement("strong");
title.textContent = action.label;
const endpoint = document.createElement("p");
endpoint.className = "muted";
endpoint.textContent = action.endpoint;
const payload = document.createElement("pre");
payload.textContent = JSON.stringify(action.payload, null, 2);
const approve = document.createElement("button");
approve.textContent = "Approve";
approve.addEventListener("click", () => approveAction(action.id));
const decline = document.createElement("button");
decline.className = "decline-button";
decline.textContent = "Decline";
decline.addEventListener("click", () => declineAction(action.id));
const controls = document.createElement("div");
controls.className = "pending-controls";
controls.append(decline, approve);
card.append(title, endpoint, payload, controls);
pendingEl.appendChild(card);
}
}
async function approveAction(id) {
statusEl.textContent = "Sending";
try {
const response = await fetch(`/api/approve/${id}`, { method: "POST" });
const result = await response.json();
addMessage("assistant", `Approval result:\n${JSON.stringify(result, null, 2)}`);
await refreshPending();
} catch (error) {
addMessage("assistant", `Approval failed: ${error.message}`);
} finally {
statusEl.textContent = "Ready";
}
}
async function declineAction(id) {
statusEl.textContent = "Declining";
try {
const response = await fetch(`/api/decline/${id}`, { method: "POST" });
const result = await response.json();
if (result.error) {
addMessage("assistant warning-message", `Decline failed: ${result.error}`);
} else {
addMessage("assistant", `Declined pending action: ${result.pending_action?.label || id}`);
}
await refreshPending();
} catch (error) {
addMessage("assistant warning-message", `Decline failed: ${error.message}`);
} finally {
statusEl.textContent = "Ready";
}
}
async function refreshPending() {
const response = await fetch("/api/pending-actions");
const result = await response.json();
renderPending(result.pending_actions || []);
}
function renderMemory(data) {
memoryInspectorEl.innerHTML = "";
const counts = document.createElement("div");
counts.className = "memory-counts";
counts.textContent = `${data.memories.length} memories | ${data.conversations.length} chat rows | ${data.profile.length} profile keys | ${data.scheduled_jobs.length} jobs`;
memoryInspectorEl.appendChild(counts);
const path = document.createElement("div");
path.className = "memory-path";
path.textContent = data.path;
memoryInspectorEl.appendChild(path);
memoryInspectorEl.appendChild(memoryGroup("Memories", data.memories, (item) => `${item.kind} (${item.importance})\n${item.content}`));
memoryInspectorEl.appendChild(memoryGroup("Profile", data.profile, (item) => `${item.key}\n${JSON.stringify(item.value, null, 2)}`));
memoryInspectorEl.appendChild(memoryGroup("Recent Chat", data.conversations, (item) => `${item.created_at} ${item.role}\n${item.content}`));
memoryInspectorEl.appendChild(memoryGroup("Wake Jobs", data.scheduled_jobs, (item) => `${item.id}\n${item.trigger_type}: ${item.trigger_value}\n${item.prompt}`));
}
function memoryGroup(title, items, formatter) {
const group = document.createElement("details");
group.className = "memory-group";
const summary = document.createElement("summary");
summary.textContent = `${title} (${items.length})`;
group.appendChild(summary);
if (!items.length) {
const empty = document.createElement("p");
empty.className = "muted";
empty.textContent = "Empty";
group.appendChild(empty);
return group;
}
for (const item of items.slice(0, 20)) {
const entry = document.createElement("pre");
entry.textContent = formatter(item);
group.appendChild(entry);
}
return group;
}
async function refreshMemory() {
try {
const response = await fetch("/api/memory?limit=50");
const data = await response.json();
renderMemory(data);
} catch (error) {
memoryInspectorEl.textContent = `Memory load failed: ${error.message}`;
}
}
async function clearMemory() {
const payload = {
include_memories: document.getElementById("clear-memories").checked,
include_conversations: document.getElementById("clear-conversations").checked,
include_profile: document.getElementById("clear-profile").checked,
include_jobs: document.getElementById("clear-jobs").checked,
include_outbox: document.getElementById("clear-outbox").checked,
};
if (!Object.values(payload).some(Boolean)) return;
const confirmed = window.confirm("Clear the selected TraderAI memory? This cannot be undone.");
if (!confirmed) return;
try {
const response = await fetch("/api/memory/clear", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const result = await response.json();
renderMemory(result.memory);
addMessage("assistant", `Memory cleared:\n${JSON.stringify(result.deleted, null, 2)}`);
} catch (error) {
addMessage("assistant warning-message", `Memory clear failed: ${error.message}`);
}
}
async function pollNotifications() {
try {
const response = await fetch("/api/notifications");
const result = await response.json();
for (const notification of result.notifications || []) {
addMessage("assistant", notification.content);
}
} catch {
// Notification polling should never interrupt chat.
}
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
await sendMessage();
});
input.addEventListener("keydown", async (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
await sendMessage();
}
});
memoryRefreshButton?.addEventListener("click", refreshMemory);
memoryClearButton?.addEventListener("click", clearMemory);
configRefreshButton?.addEventListener("click", refreshConfig);
configForm?.addEventListener("submit", saveConfig);
settingsToggle?.addEventListener("click", () => toggleSidebarPanel("settings"));
memoryToggle?.addEventListener("click", () => toggleSidebarPanel("memory"));
ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama"));
ollamaForm?.addEventListener("submit", saveOllamaConfig);
ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus);
ollamaDownloadButton?.addEventListener("click", () => postOllamaAction("/api/ollama/download"));
ollamaInstallButton?.addEventListener("click", () => postOllamaAction("/api/ollama/install"));
ollamaLaunchButton?.addEventListener("click", () => postOllamaAction("/api/ollama/launch"));
ollamaPullButton?.addEventListener("click", () => postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } }));
updateCheckButton?.addEventListener("click", checkForUpdate);
updateInstallButton?.addEventListener("click", installUpdate);
updateOpenReleasesButton?.addEventListener("click", openReleasesPage);
async function sendMessage() {
const message = input.value.trim();
if (!message || input.disabled) return;
const healthy = await checkHealth();
if (!healthy) {
addMessage("assistant warning-message", "Ollama is offline. Start Ollama, then try again.");
return;
}
input.value = "";
input.disabled = true;
addMessage("user", message);
const assistantNode = addMessage("assistant streaming", "");
ensureStreamingChrome(assistantNode);
let assistantText = "";
const thinkParser = createThinkTagParser(assistantNode);
statusEl.textContent = "Working";
setMessageActivity(assistantNode, "Thinking", true);
setMessageMetrics(assistantNode, "");
try {
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop() || "";
for (const rawEvent of events) {
const line = rawEvent.split("\n").find((entry) => entry.startsWith("data: "));
if (!line) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "status") {
setMessageActivity(assistantNode, event.message, true);
} else if (event.type === "metrics") {
setMessageMetrics(assistantNode, formatMetrics(event));
} else if (event.type === "warning") {
setWarning(event.message);
assistantText += event.message;
setMessageMarkdown(assistantNode, assistantText);
} else if (event.type === "token") {
const visibleContent = thinkParser.consume(event.content);
if (visibleContent) {
assistantText += visibleContent;
setMessageMarkdown(assistantNode, assistantText);
}
messages.scrollTop = messages.scrollHeight;
} else if (event.type === "done") {
const visibleContent = thinkParser.flush();
if (visibleContent) {
assistantText += visibleContent;
setMessageMarkdown(assistantNode, assistantText);
}
renderPending(event.pending_actions || []);
}
}
}
} catch (error) {
const message = error.message.includes("503")
? "Ollama is offline or unreachable. Start Ollama, then try again."
: `Chat failed: ${error.message}`;
setWarning(message);
setMessageMarkdown(assistantNode, message);
} finally {
assistantNode.classList.remove("streaming");
input.disabled = false;
input.focus();
statusEl.textContent = "Ready";
finishThinking(assistantNode);
setMessageActivity(assistantNode, "");
}
}
addMessage("assistant", "Tell me what to find or draft on UEX. I will ask for approval before sending anything.");
refreshPending();
refreshMemory();
refreshConfig();
refreshOllamaStatus();
checkForUpdate();
pollNotifications();
checkHealth();
setInterval(checkHealth, 30000);
setInterval(pollNotifications, 15000);