Inital Commit
This commit is contained in:
+341
@@ -0,0 +1,341 @@
|
||||
const form = document.getElementById("chat-form");
|
||||
const input = document.getElementById("message-input");
|
||||
const messages = document.getElementById("messages");
|
||||
const statusEl = document.getElementById("status");
|
||||
const pendingEl = document.getElementById("pending-actions");
|
||||
const activityEl = document.getElementById("activity");
|
||||
const warningEl = document.getElementById("warning");
|
||||
const memoryInspectorEl = document.getElementById("memory-inspector");
|
||||
const memoryRefreshButton = document.getElementById("memory-refresh");
|
||||
const memoryClearButton = document.getElementById("memory-clear");
|
||||
|
||||
let ollamaOnline = true;
|
||||
|
||||
function addMessage(role, text) {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${role}`;
|
||||
setMessageMarkdown(node, text);
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return node;
|
||||
}
|
||||
|
||||
function setMessageMarkdown(node, text) {
|
||||
node.innerHTML = renderMarkdown(text);
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
const escaped = escapeHtml(text);
|
||||
const formatted = escaped
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
const lines = formatted.split(/\n/);
|
||||
const output = [];
|
||||
let inList = false;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const item = line.match(/^(\s*)-\s+(.+)$/);
|
||||
if (item) {
|
||||
if (!inList) {
|
||||
output.push("<ul>");
|
||||
inList = true;
|
||||
}
|
||||
const nestedClass = item[1].length >= 2 ? ' class="nested"' : "";
|
||||
output.push(`<li${nestedClass}>${item[2]}</li>`);
|
||||
continue;
|
||||
}
|
||||
if (inList) {
|
||||
output.push("</ul>");
|
||||
inList = false;
|
||||
}
|
||||
if (/^---+$/.test(trimmed)) {
|
||||
output.push("<hr>");
|
||||
} else if (/^###\s+/.test(trimmed)) {
|
||||
output.push(`<h3>${trimmed.replace(/^###\s+/, "")}</h3>`);
|
||||
} else if (/^##\s+/.test(trimmed)) {
|
||||
output.push(`<h3>${trimmed.replace(/^##\s+/, "")}</h3>`);
|
||||
} else if (trimmed) {
|
||||
output.push(`<p>${line}</p>`);
|
||||
} else {
|
||||
output.push("<br>");
|
||||
}
|
||||
}
|
||||
if (inList) output.push("</ul>");
|
||||
return output.join("");
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function setActivity(text, active = false) {
|
||||
activityEl.innerHTML = "";
|
||||
if (!text) return;
|
||||
const label = document.createElement("span");
|
||||
label.textContent = text;
|
||||
activityEl.appendChild(label);
|
||||
if (active) {
|
||||
const dots = document.createElement("span");
|
||||
dots.className = "working-dots";
|
||||
dots.innerHTML = "<i></i><i></i><i></i>";
|
||||
activityEl.appendChild(dots);
|
||||
}
|
||||
}
|
||||
|
||||
function setWarning(text) {
|
||||
warningEl.hidden = !text;
|
||||
warningEl.textContent = text || "";
|
||||
}
|
||||
|
||||
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));
|
||||
card.append(title, endpoint, payload, approve);
|
||||
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 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);
|
||||
|
||||
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", "");
|
||||
let assistantText = "";
|
||||
statusEl.textContent = "Working";
|
||||
setActivity("Thinking", true);
|
||||
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") {
|
||||
setActivity(event.message, true);
|
||||
} else if (event.type === "warning") {
|
||||
setWarning(event.message);
|
||||
assistantText += event.message;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
} else if (event.type === "token") {
|
||||
assistantText += event.content;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
} else if (event.type === "done") {
|
||||
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";
|
||||
setActivity("");
|
||||
}
|
||||
}
|
||||
|
||||
addMessage("assistant", "Tell me what to find or draft on UEX. I will ask for approval before sending anything.");
|
||||
refreshPending();
|
||||
refreshMemory();
|
||||
pollNotifications();
|
||||
checkHealth();
|
||||
setInterval(checkHealth, 30000);
|
||||
setInterval(pollNotifications, 15000);
|
||||
Reference in New Issue
Block a user