feat: chat improvements, file viewer, usage fixes, and agent filesystem awareness
This commit is contained in:
@@ -358,6 +358,17 @@ async function fileBrowserNavigate(path) {
|
||||
|
||||
const actionTd = document.createElement("td");
|
||||
actionTd.style.cssText = "text-align:right;display:flex;justify-content:flex-end;gap:6px;align-items:center";
|
||||
|
||||
// View button — text files only
|
||||
if (!entry.is_dir && _fbIsTextFile(entry.name)) {
|
||||
const viewBtn = document.createElement("button");
|
||||
viewBtn.className = "btn btn-ghost btn-small";
|
||||
viewBtn.title = "View file";
|
||||
viewBtn.textContent = "View";
|
||||
viewBtn.onclick = (function(p, n) { return function(e) { e.stopPropagation(); fileBrowserViewFile(p, n); }; })(entry.path, entry.name);
|
||||
actionTd.appendChild(viewBtn);
|
||||
}
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "btn btn-ghost btn-small";
|
||||
if (entry.is_dir) {
|
||||
@@ -461,6 +472,43 @@ async function fileBrowserDeleteFile(path, name) {
|
||||
}
|
||||
}
|
||||
|
||||
const _FB_TEXT_EXTS = new Set([
|
||||
"md","txt","json","xml","yaml","yml","csv","html","htm","css","js","ts",
|
||||
"jsx","tsx","py","sh","bash","zsh","log","sql","toml","ini","conf","cfg",
|
||||
"env","gitignore","dockerfile","rst","tex","diff","patch","nfo","tsv",
|
||||
]);
|
||||
|
||||
function _fbIsTextFile(name) {
|
||||
const ext = name.includes(".") ? name.split(".").pop().toLowerCase() : "";
|
||||
return _FB_TEXT_EXTS.has(ext);
|
||||
}
|
||||
|
||||
async function fileBrowserViewFile(path, name) {
|
||||
try {
|
||||
const r = await _fbFetch("/api/my/files/view?path=" + encodeURIComponent(path));
|
||||
const data = await r.json();
|
||||
const modal = document.getElementById("file-viewer-modal");
|
||||
document.getElementById("fv-title").textContent = name;
|
||||
const pre = document.getElementById("fv-content");
|
||||
pre.textContent = data.content;
|
||||
const notice = document.getElementById("fv-truncated");
|
||||
if (data.truncated) {
|
||||
notice.textContent = "File truncated at 512 KB.";
|
||||
notice.style.display = "";
|
||||
} else {
|
||||
notice.style.display = "none";
|
||||
}
|
||||
modal.style.display = "flex";
|
||||
} catch (e) {
|
||||
alert("Could not load file: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeFileViewer() {
|
||||
const modal = document.getElementById("file-viewer-modal");
|
||||
if (modal) modal.style.display = "none";
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
CHAT PAGE
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
@@ -799,6 +847,58 @@ function _renderModelPicker(q) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Markdown renderer ───────────────────────────────────────────────────── */
|
||||
|
||||
function renderMarkdown(text) {
|
||||
const parts = [];
|
||||
const re = /```(\w*)\n?([\s\S]*?)```/g;
|
||||
let last = 0, m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
if (m.index > last) parts.push({ type: "text", s: text.slice(last, m.index) });
|
||||
parts.push({ type: "code", lang: m[1] || "", s: m[2].replace(/\n$/, "") });
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
if (last < text.length) parts.push({ type: "text", s: text.slice(last) });
|
||||
|
||||
return parts.map(p => {
|
||||
if (p.type === "code") {
|
||||
const langLabel = p.lang ? `<span class="code-lang">${esc(p.lang)}</span>` : `<span class="code-lang"></span>`;
|
||||
return `<div class="code-block">` +
|
||||
`<div class="code-block-header">${langLabel}` +
|
||||
`<button class="copy-code-btn btn btn-ghost btn-small" onclick="copyCode(this)">Copy</button>` +
|
||||
`</div><pre><code>${esc(p.s)}</code></pre></div>`;
|
||||
}
|
||||
return _renderInline(p.s);
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function _renderInline(text) {
|
||||
let s = esc(text);
|
||||
s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
s = s.replace(/`([^`\n]+)`/g, '<code class="inline-code">$1</code>');
|
||||
s = s.replace(/\n/g, "<br>");
|
||||
return s;
|
||||
}
|
||||
|
||||
function copyCode(btn) {
|
||||
const code = btn.closest(".code-block").querySelector("code").textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function copyResponse(btn) {
|
||||
const bubble = btn.closest(".message.assistant").querySelector(".message-bubble");
|
||||
const text = bubble.dataset.raw || bubble.textContent;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/* ── Event handling ─────────────────────────────────────────────────────── */
|
||||
|
||||
// Track in-progress assistant message and tool indicators by call_id
|
||||
@@ -865,15 +965,22 @@ function restoreChat(msg) {
|
||||
if (turn.role === "user") {
|
||||
const div = document.createElement("div");
|
||||
div.className = "message user";
|
||||
div.innerHTML = `<div class="message-bubble">${esc(turn.text)}</div>`;
|
||||
div.innerHTML = `<div class="message-bubble" style="white-space:normal">${renderMarkdown(turn.text)}</div>`;
|
||||
container.appendChild(div);
|
||||
} else if (turn.role === "assistant") {
|
||||
const div = document.createElement("div");
|
||||
div.className = "message assistant";
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = "message-bubble";
|
||||
bubble.textContent = turn.text;
|
||||
bubble.dataset.raw = turn.text;
|
||||
bubble.innerHTML = renderMarkdown(turn.text);
|
||||
bubble.style.whiteSpace = "normal";
|
||||
div.appendChild(bubble);
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "copy-response-btn btn btn-ghost btn-small";
|
||||
copyBtn.textContent = "Copy response";
|
||||
copyBtn.onclick = function() { copyResponse(this); };
|
||||
div.appendChild(copyBtn);
|
||||
container.appendChild(div);
|
||||
}
|
||||
}
|
||||
@@ -927,6 +1034,18 @@ function resolveToolIndicator(callId, success, result, confirmed) {
|
||||
}
|
||||
|
||||
function finishResponse(msg) {
|
||||
// Render markdown on the completed bubble and add a copy-response button
|
||||
if (currentBubble && currentAssistantDiv) {
|
||||
const raw = currentBubble.textContent;
|
||||
currentBubble.dataset.raw = raw;
|
||||
currentBubble.innerHTML = renderMarkdown(raw);
|
||||
currentBubble.style.whiteSpace = "normal";
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "copy-response-btn btn btn-ghost btn-small";
|
||||
copyBtn.textContent = "Copy response";
|
||||
copyBtn.onclick = function() { copyResponse(this); };
|
||||
currentAssistantDiv.appendChild(copyBtn);
|
||||
}
|
||||
currentAssistantDiv = null;
|
||||
currentBubble = null;
|
||||
finishGenerating();
|
||||
@@ -959,8 +1078,8 @@ function createUserMessage(text, imageDataUrls) {
|
||||
).join("");
|
||||
html += `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:${text ? "6px" : "0"}">${imgs}</div>`;
|
||||
}
|
||||
if (text) html += `<div>${esc(text)}</div>`;
|
||||
wrap.innerHTML = `<div class="message-bubble">${html}</div>`;
|
||||
if (text) html += renderMarkdown(text);
|
||||
wrap.innerHTML = `<div class="message-bubble" style="white-space:normal">${html}</div>`;
|
||||
document.getElementById("chat-messages").appendChild(wrap);
|
||||
return wrap;
|
||||
}
|
||||
@@ -1301,6 +1420,16 @@ function initUsage() {
|
||||
loadUsage();
|
||||
}
|
||||
|
||||
async function clearUsageCost() {
|
||||
if (!confirm("Clear all usage data?\n\nThis deletes all agent run history and resets chat session cost estimates. The agents themselves are not affected. This cannot be undone.")) return;
|
||||
const r = await fetch("/api/usage/cost", { method: "DELETE", credentials: "same-origin" });
|
||||
if (!r.ok) {
|
||||
alert("Failed to clear cost data.");
|
||||
return;
|
||||
}
|
||||
loadUsage();
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
AUDIT PAGE
|
||||
|
||||
Reference in New Issue
Block a user