feat: chat
This commit is contained in:
+305
-10
@@ -15,6 +15,7 @@ const configPathsEl = document.getElementById("config-paths");
|
||||
const settingsToggle = document.getElementById("settings-toggle");
|
||||
const memoryToggle = document.getElementById("memory-toggle");
|
||||
const plansToggle = document.getElementById("plans-toggle");
|
||||
const negotiationsToggle = document.getElementById("negotiations-toggle");
|
||||
const ollamaToggle = document.getElementById("ollama-toggle");
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const memoryPanel = document.getElementById("memory-panel");
|
||||
@@ -48,6 +49,32 @@ const negotiationForm = document.getElementById("negotiation-form");
|
||||
const negotiationInput = document.getElementById("negotiation-input");
|
||||
const negotiationStatusEl = document.getElementById("negotiation-status");
|
||||
const negotiationCloseButton = document.getElementById("negotiation-close");
|
||||
const negotiationListEl = document.getElementById("negotiation-list");
|
||||
const negotiationsRefreshAllButton = document.getElementById("negotiations-refresh-all");
|
||||
const negotiationPanelListEl = document.getElementById("negotiation-panel-list");
|
||||
const negotiationSearchEl = document.getElementById("negotiation-search");
|
||||
const negotiationFilterEl = document.getElementById("negotiation-filter");
|
||||
const negotiationThreadHeaderEl = document.getElementById("negotiation-thread-header");
|
||||
const negotiationMetaCardEl = document.getElementById("negotiation-meta-card");
|
||||
const negotiationUserCardEl = document.getElementById("negotiation-user-card");
|
||||
const negotiationRefreshButton = document.getElementById("negotiation-refresh-button");
|
||||
const negotiationDraftButton = document.getElementById("negotiation-draft-button");
|
||||
const negotiationOpenChatButton = document.getElementById("negotiation-open-chat");
|
||||
const negotiationEndDealButton = document.getElementById("negotiation-end-deal");
|
||||
const negotiationSyncPillEl = document.getElementById("negotiation-sync-pill");
|
||||
const negotiationCloseModal = document.getElementById("negotiation-close-modal");
|
||||
const negotiationCloseModalClose = document.getElementById("negotiation-close-modal-close");
|
||||
const negotiationCloseForm = document.getElementById("negotiation-close-form");
|
||||
const negotiationCloseStatusEl = document.getElementById("negotiation-close-status");
|
||||
const closeDealClosedEl = document.getElementById("close-deal-closed");
|
||||
const closeDealValueEl = document.getElementById("close-deal-value");
|
||||
const closeCurrencyEl = document.getElementById("close-currency");
|
||||
const closeClarityEl = document.getElementById("close-clarity");
|
||||
const closeSpeedEl = document.getElementById("close-speed");
|
||||
const closeRespectEl = document.getElementById("close-respect");
|
||||
const closeFairnessEl = document.getElementById("close-fairness");
|
||||
const closeCommentEl = document.getElementById("close-comment");
|
||||
const closeDraftButton = document.getElementById("close-draft-button");
|
||||
const updateModal = document.getElementById("update-modal");
|
||||
const updateModalCopy = document.getElementById("update-modal-copy");
|
||||
const updateModalClose = document.getElementById("update-modal-close");
|
||||
@@ -66,6 +93,7 @@ let ollamaOnline = true;
|
||||
let latestUpdate = null;
|
||||
let currentThreadId = "default";
|
||||
let currentNegotiationId = null;
|
||||
let negotiationRows = [];
|
||||
let latestOllamaStatus = null;
|
||||
let composerImages = [];
|
||||
const clickedOllamaActions = new Set();
|
||||
@@ -1182,16 +1210,114 @@ async function deleteInboxItem(id) {
|
||||
await refreshInbox();
|
||||
}
|
||||
|
||||
async function refreshNegotiations(preserveCurrent = true) {
|
||||
const status = negotiationFilterEl?.value || "open";
|
||||
const search = negotiationSearchEl?.value?.trim() || "";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations?status=${encodeURIComponent(status)}&search=${encodeURIComponent(search)}&limit=100`);
|
||||
const result = await response.json();
|
||||
negotiationRows = result.negotiations || [];
|
||||
renderNegotiationLists(negotiationRows);
|
||||
if (!preserveCurrent) return;
|
||||
if (!currentNegotiationId && negotiationRows.length) currentNegotiationId = negotiationRows[0].hash;
|
||||
if (currentNegotiationId && negotiationRows.some((item) => item.hash === currentNegotiationId) && !negotiationPanel.hidden) {
|
||||
await loadNegotiationDetail(currentNegotiationId, false);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Negotiations failed: ${fetchErrorMessage(error)}`;
|
||||
if (negotiationListEl) negotiationListEl.textContent = message;
|
||||
if (negotiationPanelListEl) negotiationPanelListEl.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllNegotiations() {
|
||||
const previous = negotiationsRefreshAllButton?.disabled;
|
||||
if (negotiationsRefreshAllButton) negotiationsRefreshAllButton.disabled = true;
|
||||
if (negotiationStatusEl) negotiationStatusEl.textContent = "Refreshing all negotiations";
|
||||
try {
|
||||
const response = await fetch("/api/negotiations/refresh-all", { method: "POST" });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
await refreshNegotiations(true);
|
||||
if (negotiationStatusEl) {
|
||||
negotiationStatusEl.textContent = `Refreshed ${result.count || 0} negotiations`;
|
||||
}
|
||||
} catch (error) {
|
||||
if (negotiationStatusEl) negotiationStatusEl.textContent = `Refresh all failed: ${fetchErrorMessage(error)}`;
|
||||
} finally {
|
||||
if (negotiationsRefreshAllButton) negotiationsRefreshAllButton.disabled = Boolean(previous);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNegotiationLists(items) {
|
||||
renderNegotiationListInto(negotiationListEl, items.slice(0, 8));
|
||||
renderNegotiationListInto(negotiationPanelListEl, items);
|
||||
}
|
||||
|
||||
function renderNegotiationListInto(container, items) {
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="pending-empty">No negotiations</div>';
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = `negotiation-row${item.hash === currentNegotiationId ? " active" : ""}`;
|
||||
row.addEventListener("click", () => openNegotiationPanel(item.hash));
|
||||
const top = document.createElement("div");
|
||||
top.className = "negotiation-row-top";
|
||||
const title = document.createElement("div");
|
||||
title.className = "negotiation-row-title";
|
||||
title.textContent = item.title || item.counterparty_username || item.hash;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `negotiation-row-badge ${item.status === "closed" ? "closed" : ""}`;
|
||||
badge.textContent = item.status || "open";
|
||||
top.append(title, badge);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "negotiation-row-meta";
|
||||
meta.textContent = [
|
||||
item.counterparty_username || "Unknown user",
|
||||
item.last_message_at ? formatShortDate(item.last_message_at) : "No messages",
|
||||
].join(" • ");
|
||||
row.append(top, meta);
|
||||
if (Number(item.unread_count || 0) > 0) {
|
||||
const unread = document.createElement("span");
|
||||
unread.className = "negotiation-row-unread";
|
||||
unread.textContent = String(item.unread_count);
|
||||
row.appendChild(unread);
|
||||
}
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function openNegotiationPanel(identifier) {
|
||||
if (!identifier) {
|
||||
negotiationPanel.hidden = false;
|
||||
negotiationsToggle?.setAttribute("aria-expanded", "true");
|
||||
return;
|
||||
}
|
||||
currentNegotiationId = identifier;
|
||||
negotiationPanel.hidden = false;
|
||||
negotiationTitle.textContent = `Negotiation ${identifier}`;
|
||||
negotiationsToggle?.setAttribute("aria-expanded", "true");
|
||||
negotiationStatusEl.textContent = "";
|
||||
negotiationSyncPillEl.textContent = "Local sync";
|
||||
await loadNegotiationDetail(identifier, true);
|
||||
}
|
||||
|
||||
async function loadNegotiationDetail(identifier, refreshList = true) {
|
||||
negotiationTitle.textContent = `Negotiation ${identifier}`;
|
||||
negotiationMessagesEl.textContent = "Loading";
|
||||
negotiationThreadHeaderEl.innerHTML = '<div class="muted">Loading local thread...</div>';
|
||||
negotiationMetaCardEl.innerHTML = "<h3>Deal</h3><div class='muted'>Loading</div>";
|
||||
negotiationUserCardEl.innerHTML = "<h3>User</h3><div class='muted'>Loading</div>";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(identifier)}/messages`);
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(identifier)}`);
|
||||
const result = await response.json();
|
||||
renderNegotiationMessages(result.data || result.messages || result.notifications || []);
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
renderNegotiationDetail(result.negotiation);
|
||||
if (refreshList) await refreshNegotiations(false);
|
||||
} catch (error) {
|
||||
negotiationMessagesEl.textContent = `Could not load negotiation: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
@@ -1202,6 +1328,7 @@ function closeNegotiationPanel() {
|
||||
currentNegotiationId = null;
|
||||
negotiationInput.value = "";
|
||||
negotiationStatusEl.textContent = "";
|
||||
negotiationsToggle?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
function openPlansPanel(openPlanId = null) {
|
||||
@@ -1217,6 +1344,37 @@ function closePlansPanel() {
|
||||
plansToggle?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
function renderNegotiationDetail(negotiation) {
|
||||
if (!negotiation) return;
|
||||
negotiationTitle.textContent = negotiation.title || negotiation.counterparty_username || negotiation.hash;
|
||||
negotiationSyncPillEl.textContent = negotiation.last_synced_at ? `Synced ${formatShortDate(negotiation.last_synced_at)}` : "Local sync";
|
||||
negotiationThreadHeaderEl.innerHTML = `
|
||||
<div><strong>${escapeHtml(negotiation.title || "Negotiation")}</strong></div>
|
||||
<div class="muted">${escapeHtml(negotiation.counterparty_username || "Unknown user")} • ${escapeHtml(negotiation.status || "open")} • ${escapeHtml(negotiation.hash || "")}</div>
|
||||
`;
|
||||
renderNegotiationMessages(negotiation.messages || []);
|
||||
const raw = negotiation.metadata?.raw || {};
|
||||
negotiationMetaCardEl.innerHTML = `
|
||||
<h3>Deal</h3>
|
||||
<div class="negotiation-detail-kv">
|
||||
<div><strong>Listing</strong> ${escapeHtml(negotiation.title || raw.listing_title || negotiation.hash)}</div>
|
||||
<div><strong>Status</strong> ${escapeHtml(negotiation.status || "open")}</div>
|
||||
<div><strong>Slug</strong> ${escapeHtml(negotiation.listing_slug || raw.listing_slug || "Unknown")}</div>
|
||||
<div><strong>Price</strong> ${escapeHtml(String(raw.price || raw.deal_value || "Unknown"))} ${escapeHtml(String(raw.currency || raw.deal_value_currency || ""))}</div>
|
||||
<div><strong>Last message</strong> ${escapeHtml(negotiation.last_message_at ? formatShortDate(negotiation.last_message_at) : "Unknown")}</div>
|
||||
</div>
|
||||
`;
|
||||
negotiationUserCardEl.innerHTML = `
|
||||
<h3>User</h3>
|
||||
<div class="negotiation-detail-kv">
|
||||
<div><strong>Counterparty</strong> ${escapeHtml(negotiation.counterparty_username || raw.client_username || raw.advertiser_username || "Unknown")}</div>
|
||||
<div><strong>Advertiser</strong> ${escapeHtml(String(raw.advertiser_username || raw.advertiser_name || "Unknown"))}</div>
|
||||
<div><strong>Client</strong> ${escapeHtml(String(raw.client_username || raw.client_name || "Unknown"))}</div>
|
||||
<div><strong>Unread</strong> ${escapeHtml(String(negotiation.unread_count || 0))}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderNegotiationMessages(data) {
|
||||
negotiationMessagesEl.innerHTML = "";
|
||||
const items = Array.isArray(data) ? data : [data].filter(Boolean);
|
||||
@@ -1226,10 +1384,15 @@ function renderNegotiationMessages(data) {
|
||||
}
|
||||
for (const item of items) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "negotiation-message";
|
||||
const author = item.user_username || item.username || item.author || item.sender || "UEX";
|
||||
const body = item.message || item.content || item.text || JSON.stringify(item, null, 2);
|
||||
card.innerHTML = `<strong>${escapeHtml(String(author))}</strong><br>${inlineMarkdown(String(body))}`;
|
||||
card.className = `negotiation-message${item.is_me ? " self" : ""}`;
|
||||
const author = item.author_username || item.user_username || item.username || item.author || item.sender || "UEX";
|
||||
const body = item.body || item.message || item.content || item.text || JSON.stringify(item, null, 2);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "negotiation-message-meta";
|
||||
meta.innerHTML = `<strong>${escapeHtml(String(author))}</strong><span>${escapeHtml(item.sent_at ? formatShortDate(item.sent_at) : "")}</span>`;
|
||||
const text = document.createElement("div");
|
||||
text.innerHTML = inlineMarkdown(String(body));
|
||||
card.append(meta, text);
|
||||
negotiationMessagesEl.appendChild(card);
|
||||
}
|
||||
negotiationMessagesEl.scrollTop = negotiationMessagesEl.scrollHeight;
|
||||
@@ -1241,7 +1404,7 @@ async function submitNegotiationMessage(event) {
|
||||
if (!text || !currentNegotiationId) return;
|
||||
negotiationStatusEl.textContent = "Sending";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/messages`, {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/messages/manual`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
@@ -1250,12 +1413,125 @@ async function submitNegotiationMessage(event) {
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
negotiationInput.value = "";
|
||||
negotiationStatusEl.textContent = result.message || "Sent";
|
||||
await openNegotiationPanel(currentNegotiationId);
|
||||
if (result.negotiation) renderNegotiationDetail(result.negotiation);
|
||||
await refreshNegotiations(false);
|
||||
} catch (error) {
|
||||
negotiationStatusEl.textContent = `Send failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function draftNegotiationMessage() {
|
||||
const text = negotiationInput.value.trim();
|
||||
if (!text || !currentNegotiationId) return;
|
||||
negotiationStatusEl.textContent = "Drafting";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/messages/draft`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
negotiationStatusEl.textContent = "Draft ready for approval";
|
||||
await refreshPending();
|
||||
} catch (error) {
|
||||
negotiationStatusEl.textContent = `Draft failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshActiveNegotiation() {
|
||||
if (!currentNegotiationId) return;
|
||||
negotiationStatusEl.textContent = "Refreshing";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/refresh`, { method: "POST" });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
if (result.negotiation) renderNegotiationDetail(result.negotiation);
|
||||
negotiationStatusEl.textContent = "Refreshed";
|
||||
await refreshNegotiations(false);
|
||||
} catch (error) {
|
||||
negotiationStatusEl.textContent = `Refresh failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function openNegotiationInChat() {
|
||||
if (!currentNegotiationId) return;
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/open-chat`, { method: "POST" });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
if (result.chat?.id) {
|
||||
currentThreadId = result.chat.id;
|
||||
await loadChatMessages(currentThreadId);
|
||||
await refreshChats();
|
||||
}
|
||||
} catch (error) {
|
||||
negotiationStatusEl.textContent = `Open chat failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function openNegotiationCloseModal() {
|
||||
if (!currentNegotiationId) return;
|
||||
negotiationCloseStatusEl.textContent = "";
|
||||
negotiationCloseModal.hidden = false;
|
||||
}
|
||||
|
||||
function closeNegotiationCloseModal() {
|
||||
negotiationCloseModal.hidden = true;
|
||||
}
|
||||
|
||||
function negotiationClosePayload() {
|
||||
return {
|
||||
deal_closed: closeDealClosedEl.value !== "false",
|
||||
deal_value: closeDealValueEl.value ? Number(closeDealValueEl.value) : null,
|
||||
currency: closeCurrencyEl.value.trim() || null,
|
||||
clarity_rating: closeClarityEl.value ? Number(closeClarityEl.value) : null,
|
||||
speed_rating: closeSpeedEl.value ? Number(closeSpeedEl.value) : null,
|
||||
respect_rating: closeRespectEl.value ? Number(closeRespectEl.value) : null,
|
||||
fairness_rating: closeFairnessEl.value ? Number(closeFairnessEl.value) : null,
|
||||
comment: closeCommentEl.value.trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitNegotiationClose(event) {
|
||||
event.preventDefault();
|
||||
if (!currentNegotiationId) return;
|
||||
negotiationCloseStatusEl.textContent = "Submitting";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/close/manual`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(negotiationClosePayload()),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
negotiationCloseStatusEl.textContent = result.message || "Submitted";
|
||||
if (result.negotiation) renderNegotiationDetail(result.negotiation);
|
||||
await refreshNegotiations(false);
|
||||
closeNegotiationCloseModal();
|
||||
} catch (error) {
|
||||
negotiationCloseStatusEl.textContent = `Close failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function draftNegotiationClose() {
|
||||
if (!currentNegotiationId) return;
|
||||
negotiationCloseStatusEl.textContent = "Drafting";
|
||||
try {
|
||||
const response = await fetch(`/api/negotiations/${encodeURIComponent(currentNegotiationId)}/close/draft`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(negotiationClosePayload()),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
negotiationCloseStatusEl.textContent = "Draft ready for approval";
|
||||
await refreshPending();
|
||||
} catch (error) {
|
||||
negotiationCloseStatusEl.textContent = `Draft failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function parsePlanItems(text) {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
@@ -1854,7 +2130,10 @@ async function pollNotifications() {
|
||||
try {
|
||||
const response = await fetch("/api/notifications");
|
||||
const result = await response.json();
|
||||
if ((result.notifications || []).length) await refreshInbox();
|
||||
if ((result.notifications || []).length) {
|
||||
await refreshInbox();
|
||||
await refreshNegotiations(true);
|
||||
}
|
||||
} catch {
|
||||
// Notification polling should never interrupt chat.
|
||||
}
|
||||
@@ -1899,6 +2178,11 @@ plansToggle?.addEventListener("click", () => {
|
||||
if (plansPanel?.hidden) openPlansPanel();
|
||||
else closePlansPanel();
|
||||
});
|
||||
negotiationsToggle?.addEventListener("click", () => {
|
||||
if (negotiationPanel?.hidden) openNegotiationPanel(currentNegotiationId || negotiationRows[0]?.hash || "");
|
||||
else closeNegotiationPanel();
|
||||
});
|
||||
negotiationsRefreshAllButton?.addEventListener("click", refreshAllNegotiations);
|
||||
ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama"));
|
||||
plansRefreshButton?.addEventListener("click", () => refreshPlans());
|
||||
plansCloseButton?.addEventListener("click", closePlansPanel);
|
||||
@@ -1935,6 +2219,15 @@ chatSidebarToggle?.addEventListener("click", toggleChatRail);
|
||||
newChatButton?.addEventListener("click", () => createChat(true));
|
||||
negotiationCloseButton?.addEventListener("click", closeNegotiationPanel);
|
||||
negotiationForm?.addEventListener("submit", submitNegotiationMessage);
|
||||
negotiationDraftButton?.addEventListener("click", draftNegotiationMessage);
|
||||
negotiationRefreshButton?.addEventListener("click", refreshActiveNegotiation);
|
||||
negotiationOpenChatButton?.addEventListener("click", openNegotiationInChat);
|
||||
negotiationEndDealButton?.addEventListener("click", openNegotiationCloseModal);
|
||||
negotiationSearchEl?.addEventListener("input", () => refreshNegotiations(false));
|
||||
negotiationFilterEl?.addEventListener("change", () => refreshNegotiations(false));
|
||||
negotiationCloseModalClose?.addEventListener("click", closeNegotiationCloseModal);
|
||||
negotiationCloseForm?.addEventListener("submit", submitNegotiationClose);
|
||||
closeDraftButton?.addEventListener("click", draftNegotiationClose);
|
||||
updateModalClose?.addEventListener("click", closeUpdatePrompt);
|
||||
updateModalReleases?.addEventListener("click", openReleasesPage);
|
||||
updateModalInstall?.addEventListener("click", installUpdate);
|
||||
@@ -2038,8 +2331,10 @@ refreshConfig();
|
||||
refreshOllamaStatus();
|
||||
refreshChats().then(() => loadChatMessages(currentThreadId));
|
||||
refreshInbox();
|
||||
refreshNegotiations(false);
|
||||
checkForUpdate(true);
|
||||
pollNotifications();
|
||||
checkHealth();
|
||||
setInterval(checkHealth, 30000);
|
||||
setInterval(pollNotifications, 15000);
|
||||
setInterval(() => refreshNegotiations(true), 15000);
|
||||
|
||||
+114
-11
@@ -25,6 +25,20 @@
|
||||
<div class="rail-heading">Chats</div>
|
||||
<div class="chat-list" id="chat-list"></div>
|
||||
</section>
|
||||
<section class="chat-nav-section">
|
||||
<div class="rail-heading-row">
|
||||
<div class="rail-heading">Negotiations</div>
|
||||
<div class="rail-heading-actions">
|
||||
<button class="rail-icon-button" id="negotiations-refresh-all" type="button" title="Refresh all negotiations">
|
||||
<i data-lucide="refresh-cw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="rail-icon-button" id="negotiations-toggle" type="button" title="Negotiations" aria-expanded="false" aria-controls="negotiation-panel">
|
||||
<i data-lucide="messages-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plans-rail-list" id="negotiation-list"></div>
|
||||
</section>
|
||||
<section class="chat-nav-section">
|
||||
<div class="rail-heading-row">
|
||||
<div class="rail-heading">Plans</div>
|
||||
@@ -82,6 +96,7 @@
|
||||
<label>UEX API URL<input id="config-uex-base-url" name="uex_base_url" type="text"></label>
|
||||
<label>UEX Secret Key<input id="config-uex-secret-key" name="uex_secret_key" type="password" autocomplete="off"></label>
|
||||
<label>UEX Bearer Token<input id="config-uex-bearer-token" name="uex_bearer_token" type="password" autocomplete="off"></label>
|
||||
<label>UEX Close Endpoint<input id="config-uex-negotiation-close-endpoint" name="uex_negotiation_close_endpoint" type="text"></label>
|
||||
<label>UEX Username<input id="config-traderai-user-name" name="traderai_user_name" type="text"></label>
|
||||
<label>Memory DB Path<input id="config-traderai-memory-path" name="traderai_memory_path" type="text"></label>
|
||||
<label>Notification Poll Seconds<input id="config-uex-notification-poll-seconds" name="uex_notification_poll_seconds" type="number" min="15" step="15"></label>
|
||||
@@ -170,19 +185,107 @@
|
||||
<div class="floating-panel" id="negotiation-panel" hidden>
|
||||
<div class="floating-panel-header">
|
||||
<div>
|
||||
<p class="eyebrow">UEX negotiation</p>
|
||||
<h2 id="negotiation-title">Negotiation</h2>
|
||||
<p class="eyebrow">UEX negotiations</p>
|
||||
<h2 id="negotiation-title">Negotiation workspace</h2>
|
||||
</div>
|
||||
<div class="floating-panel-actions">
|
||||
<div class="negotiation-sync-pill" id="negotiation-sync-pill">Local sync</div>
|
||||
<button class="icon-button light" id="negotiation-close" type="button" title="Close">
|
||||
<i data-lucide="x" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="icon-button light" id="negotiation-close" type="button" title="Close">
|
||||
<i data-lucide="x" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="negotiation-messages" id="negotiation-messages"></div>
|
||||
<form class="negotiation-composer" id="negotiation-form">
|
||||
<textarea id="negotiation-input" rows="2" placeholder="Reply to the other party..."></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
<div class="config-status" id="negotiation-status"></div>
|
||||
<div class="negotiation-workspace">
|
||||
<aside class="negotiation-sidebar">
|
||||
<div class="negotiation-sidebar-controls">
|
||||
<input id="negotiation-search" type="text" placeholder="Search negotiations">
|
||||
<select id="negotiation-filter">
|
||||
<option value="open">Open</option>
|
||||
<option value="all">All</option>
|
||||
<option value="closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="negotiation-list-panel" id="negotiation-panel-list"></div>
|
||||
</aside>
|
||||
<section class="negotiation-thread-shell">
|
||||
<div class="negotiation-thread-header" id="negotiation-thread-header">
|
||||
<div class="muted">Select a negotiation to load the local thread.</div>
|
||||
</div>
|
||||
<div class="negotiation-messages" id="negotiation-messages"></div>
|
||||
<form class="negotiation-composer" id="negotiation-form">
|
||||
<textarea id="negotiation-input" rows="2" placeholder="Reply to the other party..."></textarea>
|
||||
<div class="negotiation-composer-actions">
|
||||
<button class="secondary small-button" id="negotiation-draft-button" type="button">Ask AI to Draft</button>
|
||||
<button type="submit">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="config-status" id="negotiation-status"></div>
|
||||
</section>
|
||||
<aside class="negotiation-detail-rail">
|
||||
<div class="negotiation-detail-card" id="negotiation-meta-card">
|
||||
<h3>Deal</h3>
|
||||
<div class="muted">No negotiation selected.</div>
|
||||
</div>
|
||||
<div class="negotiation-detail-card" id="negotiation-user-card">
|
||||
<h3>User</h3>
|
||||
<div class="muted">No negotiation selected.</div>
|
||||
</div>
|
||||
<div class="negotiation-detail-card">
|
||||
<h3>Actions</h3>
|
||||
<div class="negotiation-action-stack">
|
||||
<button class="secondary small-button" id="negotiation-open-chat" type="button">Open in AI Chat</button>
|
||||
<button class="small-button" id="negotiation-refresh-button" type="button">Refresh Thread</button>
|
||||
<button class="danger-button small-button" id="negotiation-end-deal" type="button">End Deal</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" id="negotiation-close-modal" hidden>
|
||||
<section class="update-modal-card negotiation-close-card">
|
||||
<div class="section-title-row">
|
||||
<h2>End Deal</h2>
|
||||
<button class="icon-button light" id="negotiation-close-modal-close" type="button" title="Close">
|
||||
<i data-lucide="x" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form class="config-form" id="negotiation-close-form">
|
||||
<label>Did you close a deal?
|
||||
<select id="close-deal-closed">
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="plan-form-split">
|
||||
<label>Deal value
|
||||
<input id="close-deal-value" type="number" min="0" step="1" placeholder="1000000">
|
||||
</label>
|
||||
<label>Currency
|
||||
<input id="close-currency" type="text" value="UEC">
|
||||
</label>
|
||||
</div>
|
||||
<label>Clear, timely, and honest?
|
||||
<input id="close-clarity" type="number" min="1" max="5" step="1" value="5">
|
||||
</label>
|
||||
<label>Delivery or response time?
|
||||
<input id="close-speed" type="number" min="1" max="5" step="1" value="5">
|
||||
</label>
|
||||
<label>Respectful and easy to deal with?
|
||||
<input id="close-respect" type="number" min="1" max="5" step="1" value="5">
|
||||
</label>
|
||||
<label>Price or offer fairness?
|
||||
<input id="close-fairness" type="number" min="1" max="5" step="1" value="5">
|
||||
</label>
|
||||
<label>Comments
|
||||
<textarea id="close-comment" rows="3" placeholder="Optional note"></textarea>
|
||||
</label>
|
||||
<div class="plan-form-actions">
|
||||
<button class="secondary" id="close-draft-button" type="button">Draft for Approval</button>
|
||||
<button type="submit">Rate Deal</button>
|
||||
</div>
|
||||
<div class="config-status" id="negotiation-close-status"></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="floating-panel plans-floating-panel" id="plans-panel" hidden>
|
||||
<div class="floating-panel-header">
|
||||
|
||||
+219
-2
@@ -105,7 +105,7 @@ body::before {
|
||||
|
||||
.chat-rail-content {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(92px, 20%) minmax(130px, 30%);
|
||||
grid-template-rows: minmax(0, 1.7fr) minmax(72px, 0.45fr) minmax(92px, 0.65fr) minmax(120px, 0.95fr);
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding-top: 16px;
|
||||
@@ -139,6 +139,12 @@ body::before {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rail-heading-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rail-heading-row .rail-heading {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -962,6 +968,26 @@ button {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
#negotiation-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
width: min(1280px, calc(100vw - 28px));
|
||||
max-height: min(860px, calc(100vh - 56px));
|
||||
}
|
||||
|
||||
.negotiation-sync-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(240, 214, 129, 0.32);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 250, 240, 0.12);
|
||||
color: var(--gold-2);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.floating-panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -977,6 +1003,114 @@ button {
|
||||
color: var(--ivory);
|
||||
}
|
||||
|
||||
.negotiation-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr) 260px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.negotiation-sidebar,
|
||||
.negotiation-thread-shell,
|
||||
.negotiation-detail-rail {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.negotiation-sidebar {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
border-right: 1px solid var(--line);
|
||||
background: rgba(255, 250, 240, 0.58);
|
||||
}
|
||||
|
||||
.negotiation-sidebar-controls {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.negotiation-list-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.negotiation-row {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 253, 247, 0.86);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.negotiation-row.active {
|
||||
border-color: rgba(52, 83, 38, 0.42);
|
||||
background: #edf3df;
|
||||
}
|
||||
|
||||
.negotiation-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.negotiation-row-title {
|
||||
color: var(--forest);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.negotiation-row-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.negotiation-row-badge {
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: #edf3df;
|
||||
color: var(--forest);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.negotiation-row-badge.closed {
|
||||
background: #efe6ce;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.negotiation-row-unread {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--forest);
|
||||
color: var(--ivory);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.negotiation-thread-shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.negotiation-thread-header {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(255, 253, 247, 0.82);
|
||||
}
|
||||
|
||||
.negotiation-messages {
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
@@ -993,9 +1127,25 @@ button {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.negotiation-message.self {
|
||||
border-color: rgba(52, 83, 38, 0.28);
|
||||
background: #edf3df;
|
||||
}
|
||||
|
||||
.negotiation-message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.negotiation-composer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
@@ -1006,6 +1156,64 @@ button {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.negotiation-composer-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.negotiation-composer-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.negotiation-detail-rail {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-left: 1px solid var(--line);
|
||||
background: rgba(255, 250, 240, 0.52);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.negotiation-detail-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 253, 247, 0.86);
|
||||
}
|
||||
|
||||
.negotiation-detail-card h3 {
|
||||
margin: 0;
|
||||
color: var(--forest);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.negotiation-detail-kv {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.negotiation-detail-kv div {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.negotiation-detail-kv strong {
|
||||
color: var(--brown);
|
||||
}
|
||||
|
||||
.negotiation-action-stack {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.negotiation-close-card {
|
||||
width: min(520px, 100%);
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -2077,4 +2285,13 @@ pre {
|
||||
.plans-panel-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.negotiation-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.negotiation-sidebar,
|
||||
.negotiation-detail-rail {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user