feat(docs): save chat answers as docs — edit screen, commit + push to the .env docs branch
This commit is contained in:
+105
-73
@@ -9,7 +9,15 @@
|
||||
* Contract (phase 39 locked decisions — A11 no CDN, runtime fetch):
|
||||
* • window.BOR_BRAND = "Brain of Reese" synchronously — the default
|
||||
* name renders immediately, no blank flash;
|
||||
* • fetch("/api/config", { cache: "no-store" }) — on success with a
|
||||
* • Phase 59: window.BOR_DOCS_REPO_CONFIGURED = false synchronously
|
||||
* (inert until proven) and window.BOR_CONFIG_PROMISE — the SAME
|
||||
* fetch's promise, exposed at parse time so the chat page's boot
|
||||
* (app.js) can await it BEFORE rendering any bubble; the "Save as
|
||||
* doc" gating flag is then final, and a restored conversation of a
|
||||
* configured admin never misses (or flashes) the button. The
|
||||
* promise NEVER rejects — the error arm warns and resolves null;
|
||||
* • fetch("/api/config", { cache: "no-store" }) — on success the
|
||||
* docs flag is set from cfg.docs_repo_configured, and on a
|
||||
* non-empty app_name, window.BOR_BRAND is updated and the name is
|
||||
* applied to the DOM:
|
||||
* 1. document.title — global replace of the literal;
|
||||
@@ -36,6 +44,12 @@
|
||||
reading window.BOR_BRAND at evaluation time always find a value. */
|
||||
window.BOR_BRAND = "Brain of Reese";
|
||||
|
||||
/* Phase 59 (owner-locked 2026-08-31, TODO.md L3): the docs-push flag —
|
||||
surfaced the way app_name is (a window global, inert until the boot
|
||||
fetch proves otherwise). false = the "Save as doc" action is hidden
|
||||
for everyone (BOR_DOCS_REPO empty — the feature is off). */
|
||||
window.BOR_DOCS_REPO_CONFIGURED = false;
|
||||
|
||||
/* The literal the DOM passes replace — the default name. The page
|
||||
scripts' own `window.BOR_BRAND || "Brain of Reese"` fallbacks stay in
|
||||
sync with it. */
|
||||
@@ -50,83 +64,101 @@ function escapeHTML(s) {
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function applyBrand() {
|
||||
fetch("/api/config", { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
||||
.then((cfg) => {
|
||||
const name = typeof cfg?.app_name === "string" ? cfg.app_name.trim() : "";
|
||||
if (!name) return; // empty / missing: the default stands
|
||||
window.BOR_BRAND = name;
|
||||
|
||||
// 1. The document title (global replace of the literal — covers
|
||||
// every page's static "<…> · Brain of Reese" titles).
|
||||
document.title = document.title.replaceAll(BRAND_LITERAL, name);
|
||||
|
||||
// 2. The header brand on every page: a name starting "Brain of "
|
||||
// keeps the bold split (the current look), anything else
|
||||
// renders plain — the name is always escaped.
|
||||
for (const el of document.querySelectorAll(".brand-text")) {
|
||||
if (name.startsWith("Brain of ")) {
|
||||
const rest = name.slice("Brain of ".length);
|
||||
el.innerHTML = `Brain of <strong>${escapeHTML(rest)}</strong>`;
|
||||
} else {
|
||||
el.textContent = name;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Prose: a TreeWalker over the body's text nodes replaces the
|
||||
// literal (the empty-state h1, any other copy). Text nodes
|
||||
// inside <script>/<style> are rejected — the page source must
|
||||
// never be rewritten.
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
const tag = node.parentElement ? node.parentElement.tagName : "";
|
||||
return tag === "SCRIPT" || tag === "STYLE"
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
},
|
||||
);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
for (const node of nodes) {
|
||||
if (node.nodeValue && node.nodeValue.includes(BRAND_LITERAL)) {
|
||||
node.nodeValue = node.nodeValue.replaceAll(BRAND_LITERAL, name);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Attributes: the #messages aria-label, the composer input
|
||||
// label, the meta descriptions — aria-label / placeholder /
|
||||
// meta content only, each replaced in place.
|
||||
for (const el of document.querySelectorAll(
|
||||
"[aria-label], [placeholder], meta[content]",
|
||||
)) {
|
||||
for (const attr of ["aria-label", "placeholder"]) {
|
||||
const v = el.getAttribute(attr);
|
||||
if (v && v.includes(BRAND_LITERAL)) {
|
||||
el.setAttribute(attr, v.replaceAll(BRAND_LITERAL, name));
|
||||
}
|
||||
}
|
||||
if (el.tagName === "META") {
|
||||
const v = el.getAttribute("content");
|
||||
if (v && v.includes(BRAND_LITERAL)) {
|
||||
el.setAttribute("content", v.replaceAll(BRAND_LITERAL, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
/* The /api/config fetch — started at TOP LEVEL (parse time) so
|
||||
window.BOR_CONFIG_PROMISE exists before the page's module scripts
|
||||
evaluate (app.js's boot awaits it, above). Phase 59: the flag lands
|
||||
here, the moment the answer arrives — before any DOM pass. The
|
||||
promise NEVER rejects: the error arm warns (the loadHealth house
|
||||
style — the page never breaks) and resolves to null, so the default
|
||||
name + false flag stand. */
|
||||
const BOR_CONFIG_PROMISE = fetch("/api/config", { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
||||
.then(
|
||||
(cfg) => {
|
||||
window.BOR_DOCS_REPO_CONFIGURED = cfg?.docs_repo_configured === true;
|
||||
return cfg;
|
||||
},
|
||||
(err) => {
|
||||
// Fetch failure (or a non-JSON body): the default name stays —
|
||||
// the page never breaks (the loadHealth house style).
|
||||
console.warn("brand: /api/config did not answer — keeping the default name.", err);
|
||||
});
|
||||
return null;
|
||||
},
|
||||
);
|
||||
window.BOR_CONFIG_PROMISE = BOR_CONFIG_PROMISE;
|
||||
|
||||
function applyBrand() {
|
||||
BOR_CONFIG_PROMISE.then((cfg) => {
|
||||
const name = typeof cfg?.app_name === "string" ? cfg.app_name.trim() : "";
|
||||
if (!name) return; // empty / missing: the default stands
|
||||
window.BOR_BRAND = name;
|
||||
|
||||
// 1. The document title (global replace of the literal — covers
|
||||
// every page's static "<…> · Brain of Reese" titles).
|
||||
document.title = document.title.replaceAll(BRAND_LITERAL, name);
|
||||
|
||||
// 2. The header brand on every page: a name starting "Brain of "
|
||||
// keeps the bold split (the current look), anything else
|
||||
// renders plain — the name is always escaped.
|
||||
for (const el of document.querySelectorAll(".brand-text")) {
|
||||
if (name.startsWith("Brain of ")) {
|
||||
const rest = name.slice("Brain of ".length);
|
||||
el.innerHTML = `Brain of <strong>${escapeHTML(rest)}</strong>`;
|
||||
} else {
|
||||
el.textContent = name;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Prose: a TreeWalker over the body's text nodes replaces the
|
||||
// literal (the empty-state h1, any other copy). Text nodes
|
||||
// inside <script>/<style> are rejected — the page source must
|
||||
// never be rewritten.
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
const tag = node.parentElement ? node.parentElement.tagName : "";
|
||||
return tag === "SCRIPT" || tag === "STYLE"
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
},
|
||||
);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
for (const node of nodes) {
|
||||
if (node.nodeValue && node.nodeValue.includes(BRAND_LITERAL)) {
|
||||
node.nodeValue = node.nodeValue.replaceAll(BRAND_LITERAL, name);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Attributes: the #messages aria-label, the composer input
|
||||
// label, the meta descriptions — aria-label / placeholder /
|
||||
// meta content only, each replaced in place.
|
||||
for (const el of document.querySelectorAll(
|
||||
"[aria-label], [placeholder], meta[content]",
|
||||
)) {
|
||||
for (const attr of ["aria-label", "placeholder"]) {
|
||||
const v = el.getAttribute(attr);
|
||||
if (v && v.includes(BRAND_LITERAL)) {
|
||||
el.setAttribute(attr, v.replaceAll(BRAND_LITERAL, name));
|
||||
}
|
||||
}
|
||||
if (el.tagName === "META") {
|
||||
const v = el.getAttribute("content");
|
||||
if (v && v.includes(BRAND_LITERAL)) {
|
||||
el.setAttribute("content", v.replaceAll(BRAND_LITERAL, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* The top level only sets the global (synchronously, at parse time);
|
||||
the DOM passes run once the document is ready. */
|
||||
/* The DOM passes run once the document is ready AND the config is
|
||||
settled (applyBrand awaits the parse-time promise) — the fetch may
|
||||
resolve before or after DOMContentLoaded; both orderings apply the
|
||||
brand exactly once. */
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", applyBrand);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user