diff --git a/api/og.sha b/api/og.sha index 52e38d0..df01c9b 100644 --- a/api/og.sha +++ b/api/og.sha @@ -1 +1 @@ -b09b0bd5e9cb5619888cb6fe73af6a2a6d769ae371b2e05c17e54eca1b5c1956 +51f9ffaa661399ba3ea9a65459ec5f995969ccb5407c0d6ada0a09c89fc1e51a diff --git a/billing/og.sha b/billing/og.sha index c9e8574..f2c9418 100644 --- a/billing/og.sha +++ b/billing/og.sha @@ -1 +1 @@ -6afedc20df64916ab5d6f8828eb235da1de32029baeb07fd4c288e73b4e7b74f +02e0153705f4f4e86cacd1cbb8974aec050467ffcf0d4de9ee123d9d1ae5da21 diff --git a/chat.css b/chat.css index d9413a9..e5bf5d8 100644 --- a/chat.css +++ b/chat.css @@ -93,6 +93,24 @@ html[data-chat-waiting]::after{content:"ask \00b7 " attr(data-chat-waiting);pos /* A finished answer, as the tooling prints a finished step: a rail in the terminal's green and a head line with the tick, on a card a step lighter than the terminal, so the one thing the model wrote stands apart from the question and the menus around it. It has no animation of any kind. */ +/* A question a link carries, waiting for the visitor: a card framed as an answer is, its edge the + accent at 3px where an answer's is the state color at 1.5px, its line in the accent as a thing + to act on, and one ring as it arrives. The accent is what every control is; --t-part is not + used, since in this panel it underlines a claim the evidence carries only in part. */ +.rbchat-ask{margin:.2rem 0 1rem;display:grid;gap:.55rem;padding:.7rem .9rem .8rem;background:var(--t-card);border:1px solid var(--t-edge);border-left:3px solid var(--t-accent);border-radius:0 8px 8px 0} +.rbchat-ask p{margin:0} +.rbchat-ask-from{color:var(--t-accent)} +.rbchat-ask-q{color:var(--t-strong);white-space:pre-wrap;overflow-wrap:anywhere;background:var(--t-sel);padding:.45rem .7rem;border-radius:6px} +.rbchat-ask-q::before{content:"\203a ";color:var(--t-accent)} +.rbchat-ask-row{display:flex;gap:.45rem;flex-wrap:wrap} +.rbchat-ask-row button{font:inherit;font-size:.95em;padding:.3rem .65rem;border-radius:6px;border:1px solid var(--t-line);background:transparent;color:var(--t-ink);cursor:pointer} +.rbchat-ask-row button:hover{border-color:var(--t-accent)} +.rbchat-ask-row button:focus-visible{outline:2px solid var(--t-accent);outline-offset:2px} +.rbchat-ask-row .rbchat-ask-go{background:var(--t-accent);border-color:var(--t-accent);color:var(--t-bg);font-weight:600} +@media (prefers-reduced-motion:no-preference){ + .rbchat-ask{animation:rbchat-ask-ring 1.6s ease-out .3s 1 both} + @keyframes rbchat-ask-ring{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--t-accent) 55%,transparent)}100%{box-shadow:0 0 0 10px color-mix(in srgb,var(--t-accent) 0%,transparent)}} +} .rbchat-assistant{margin:.4rem 0 0 .35rem;padding:.55rem .9rem .55rem .95rem;border-left:1.5px solid var(--t-good);background:var(--t-card);border-radius:0 8px 8px 0} .rbchat-done{margin:0 0 .35rem;color:var(--t-dim)} .rbchat-tick{color:var(--t-good)} diff --git a/chat.js b/chat.js index 2309010..6f38dee 100644 --- a/chat.js +++ b/chat.js @@ -44,6 +44,7 @@ // rbChat.heard(turns) every name and cite the conversation's answers brought // rbChat.refocus(window) whether the cursor goes back after an answer // rbChat.asked(location.search) the address without ?chat=open, or null if it has none +// rbChat.linkQuestion(location.search) the question ?ask= carries and the address without it, or null // rbChat.when(retryAt, now, lang) when a limit lifts, in the visitor's language and time // rbChat.refusalText(code, retryAt, …) the refusal sentence, ending with that moment where there is one // rbChat.citeLine(cites, model, icon, doc) the line under an answer: the icon, each title, each mark @@ -127,6 +128,7 @@ again: { sentence: "You can ask again {when}.", minute: "in a minute", minutes: "in {n} minutes", at: "at {time}", tomorrow: "tomorrow at {time}", day: "on {day} at {time}" }, github: "{title} on GitHub", commit: "commit {sha}", modalClose: "Close \u00b7 Esc", + ask: { from: "The link you opened asks:", label: "A question from the link you opened", send: "Send", edit: "Edit", discard: "Discard" }, diagram: { concepts: "Concepts", process: "Process", neighborhood: "Connections", schema: "Meta-model", context: "Context map", aggregate: "Aggregate", flow: "Flow", lifecycle: "Lifecycle", organization: "Organization", reading: { context: "One-way arrows run from upstream to downstream; each arrow names the pattern between the two contexts.", aggregate: "The root holds what the diamonds join; the dashed arrows are the events it emits.", flow: "Solid arrows are commands sent to the aggregate, dashed arrows the events it emits; a box names the condition of each branch.", lifecycle: "Each arrow is a step from one state to the next, labeled with the command that takes it where there is one.", organization: "Solid arrows run from a lead to the leads below; a dashed line joins staff to the head they serve; a dashed box is an open position, beside a lead the search for a successor; agents stand in the shaded frame." }, expand: "Open full screen", shut: "Close full screen", zoomIn: "Zoom in", zoomOut: "Zoom out", fit: "Fit", fitTip: "Fit to the screen", failed: "The diagram could not be drawn; this is its source." }, refusal: { too_long: "That message is over 1,000 characters.", @@ -174,6 +176,7 @@ again: { sentence: "Sie können {when} wieder fragen.", minute: "in einer Minute", minutes: "in {n} Minuten", at: "um {time}", tomorrow: "morgen um {time}", day: "am {day} um {time}" }, github: "{title} auf GitHub", commit: "Commit {sha}", modalClose: "Schliessen \u00b7 Esc", + ask: { from: "Der Link, den Sie geöffnet haben, enthält diese Frage:", label: "Eine Frage aus dem Link, den Sie geöffnet haben", send: "Senden", edit: "Bearbeiten", discard: "Verwerfen" }, diagram: { concepts: "Konzepte", process: "Prozess", neighborhood: "Verbindungen", schema: "Meta-Modell", context: "Context Map", aggregate: "Aggregat", flow: "Ablauf", lifecycle: "Lebenszyklus", organization: "Organisation", reading: { context: "Einfache Pfeile laufen vom Upstream- zum Downstream-Kontext; jeder Pfeil nennt das Muster zwischen den beiden Kontexten.", aggregate: "Die Wurzel des Aggregats hält, was die Rauten verbinden; die gestrichelten Pfeile sind die Ereignisse, die sie auslöst.", flow: "Durchgezogene Pfeile sind Befehle an das Aggregat, gestrichelte die Ereignisse, die es auslöst; ein Kasten nennt die Bedingung jedes Zweigs.", lifecycle: "Jeder Pfeil ist ein Schritt von einem Zustand zum nächsten, beschriftet mit dem Befehl, der ihn auslöst, wo es einen gibt.", organization: "Durchgezogene Pfeile laufen von einer Führungskraft zu den Führungskräften darunter; eine gestrichelte Linie verbindet eine Stabsstelle mit der Leitung, der sie zuarbeitet; ein gestrichelter Kasten ist eine offene Stelle, neben einer Führungskraft die Suche nach ihrer Nachfolge; Software-Agenten stehen im farbig hinterlegten Rahmen." }, expand: "Im Vollbild öffnen", shut: "Vollbild schliessen", zoomIn: "Vergrössern", zoomOut: "Verkleinern", fit: "Einpassen", fitTip: "Auf den Bildschirm einpassen", failed: "Das Diagramm konnte nicht gezeichnet werden; dies ist seine Quelle." }, refusal: { too_long: "Diese Nachricht ist länger als 1’000 Zeichen.", @@ -525,6 +528,21 @@ return search.replace(re, "$1").replace(/[?&]$/, ""); } + // A link may carry a question too, with ?ask=, so a post can send a reader to one answer. The + // question is never sent on arrival: the panel opens with it waiting under the notice that says + // nothing is sent until the visitor sends, and the visitor sends, edits or discards it. Read + // once and taken out of the address, as chat=open is. This gives the question, trimmed, and the + // query string without the parameter; the question is null where it is empty, longer than a + // message may be, or not decodable, and the whole is null where the address carries no ask. + function linkQuestion(search){ + var re = /([?&])ask=([^&#]*)(&|$)/, m = re.exec(search || ""); + if (!m) return null; + var question = null; + try { question = decodeURIComponent(m[2].replace(/\+/g, " ")).trim(); } catch (e) { question = null; } + if (!question || question.length > LIMIT) question = null; + return { question: question, rest: search.replace(re, "$1").replace(/[?&]$/, "") }; + } + // n items of a list, at random and without repeats: a Fisher–Yates shuffle of a copy, cut to // n, which is uniform over every ordering and never picks the same item twice even where the // caller asks for more than the list holds — it then gives back the whole list, shuffled. @@ -876,7 +894,7 @@ // The rows a number picks, as the keys line and /help name them: none, one, or a range. function rangeOf(n){ return n > 1 ? "1-" + n : n === 1 ? "1" : ""; } - window.rbChat = { md: md, readEvents: readEvents, strings: strings, link: link, refocus: refocus, asked: asked, nameLinks: nameLinks, heard: heard, when: when, refusalText: refusalText, citeLine: citeLine, iconOf: iconOf, pick: pick, unasked: unasked, sayIn: sayIn, asTitles: asTitles, answerLang: answerLang, spread: spread, mentioned: mentioned, mermaidConfig: mermaidConfig, nodeElement: nodeElement, diagramCaption: diagramCaption, nodeHref: nodeHref, oriented: oriented, follow: follow, place: place, placed: placed, lockupOf: lockupOf, command: command, picked: picked, tryRows: tryRows, commitOf: commitOf, seconds: seconds, rangeOf: rangeOf, versionsOf: versionsOf, graphHref: graphHref, entityOf: entityOf, graphTarget: graphTarget }; + window.rbChat = { md: md, readEvents: readEvents, strings: strings, link: link, refocus: refocus, asked: asked, linkQuestion: linkQuestion, nameLinks: nameLinks, heard: heard, when: when, refusalText: refusalText, citeLine: citeLine, iconOf: iconOf, pick: pick, unasked: unasked, sayIn: sayIn, asTitles: asTitles, answerLang: answerLang, spread: spread, mentioned: mentioned, mermaidConfig: mermaidConfig, nodeElement: nodeElement, diagramCaption: diagramCaption, nodeHref: nodeHref, oriented: oriented, follow: follow, place: place, placed: placed, lockupOf: lockupOf, command: command, picked: picked, tryRows: tryRows, commitOf: commitOf, seconds: seconds, rangeOf: rangeOf, versionsOf: versionsOf, graphHref: graphHref, entityOf: entityOf, graphTarget: graphTarget }; // ─── The page ───────────────────────────────────────────────────────────────────────────── var tag = document.currentScript; @@ -1437,6 +1455,7 @@ // `messages` is what the server sees, `turns` the same exchange as the panel shows it: an // answer's cites are the widget's to draw and are no part of a message. + var linkAsk = null, askEl = null; var messages = [], turns = [], busy = false, panel = null, log = null, input = null, notice = null, title = null, closeBtn = null, grip = null, newBtn = null, keysEl = null, say = null; // The place a restore owes the visitor, held until the log is shown and every picture above it // has drawn, and let go the moment the visitor scrolls, sends or starts afresh: from then on @@ -1756,6 +1775,34 @@ // What the intro drew at random, kept for its conversation, so a language switch redraws the // same process and questions in the other language rather than drawing again. var introPick = null; + // The question a link carries, waiting: a card the visitor acts on, built from the strings and + // never from markup, so whatever the link says is shown as text. Send hands it to send() as if + // typed, Edit puts it in the input to change, and Discard drops it. Under the notice while the + // intro stands, since that is what the visitor reads first; at the end of a conversation the + // tab already holds, since the intro is then out of sight above it. + function askCard(){ + var s = strings(langNow()).ask, card = el("div", "rbchat-ask"), row = el("div", "rbchat-ask-row"); + card.setAttribute("role", "group"); card.setAttribute("aria-label", s.label); + card.appendChild(el("p", "rbchat-ask-from", s.from)); + card.appendChild(el("p", "rbchat-ask-q", linkAsk)); + var go = el("button", "rbchat-ask-go", s.send), edit = el("button", "", s.edit), drop = el("button", "", s.discard); + go.type = edit.type = drop.type = "button"; + go.appendChild(el("span", "rbchat-ask-key", " \u23ce")).setAttribute("aria-hidden", "true"); + edit.setAttribute("data-ask", "edit"); drop.setAttribute("data-ask", "discard"); + row.appendChild(go); row.appendChild(edit); row.appendChild(drop); card.appendChild(row); + function done(){ var q = linkAsk; linkAsk = null; askEl = null; if (card.parentNode) card.parentNode.removeChild(card); return q; } + go.addEventListener("click", function(){ input.value = done(); send(); }); + edit.addEventListener("click", function(){ input.value = done(); input.focus(); }); + drop.addEventListener("click", function(){ done(); if (refocus(window)) input.focus(); }); + askEl = card; + return card; + } + function showAsk(){ + if (!linkAsk || (askEl && askEl.isConnected)) return; + log.appendChild(askCard()); + log.scrollTop = log.scrollHeight; + } + function intro(play){ introRun++; var mine = introRun, s = strings(langNow()), lock = lockupOf(document); @@ -1777,6 +1824,7 @@ hello.appendChild(el("span", "rbchat-h2", lines[1])); introEl.appendChild(hello); notice = el("p", "rbchat-notice"); introEl.appendChild(notice); writeNotice(); + if (linkAsk && !messages.length) introEl.appendChild(askCard()); var groups = el("div", "rbchat-groups"); introEl.appendChild(groups); var promptLine = el("p", "rbchat-prompt-line", "› " + s.prompt + " "); // The block cursor waits here while the intro plays, and hands over to the command line. @@ -2311,11 +2359,15 @@ // click: open() focuses the input, which on a touch screen would raise the keyboard over a page // the visitor has not read yet, so there the focus is taken back at once, where refocus says. (function arrive(){ - var rest = asked(location.search); - if (rest === null) return; + var rest = asked(location.search), link = linkQuestion(rest === null ? location.search : rest); + if (rest === null && !link) return; + if (link) { rest = link.rest; linkAsk = link.question; } try { history.replaceState(null, "", location.pathname + rest + location.hash); } catch (err) {} - if (panel && !panel.hidden) return; - open(); - if (!refocus(window)) input.blur(); + if (!panel || panel.hidden) open(); + showAsk(); + // A waiting question takes the focus on a desk, so Enter sends it; on a touch screen nothing + // keeps it, as the panel opened by an address never raises the keyboard. + if (!refocus(window)) { input.blur(); return; } + if (askEl) askEl.querySelector(".rbchat-ask-go").focus({ preventScroll: true }); })(); })(); diff --git a/model/og.sha b/model/og.sha index b4c0a76..5065e71 100644 --- a/model/og.sha +++ b/model/og.sha @@ -1 +1 @@ -c21e12c6bfeb3972b496f8c6771d4b1d68379c005ab6dafae2509e36468edf6f +62621dc202a81b9f3eef57a00d2a07ddede86be2669fda1572b8b950df692ea9 diff --git a/og.sha b/og.sha index d09166c..953d3da 100644 --- a/og.sha +++ b/og.sha @@ -1 +1 @@ -47552f53e2a220a00e53d55ceeb1d6ac1d9b78f1e520308f3e490391ad8a4d86 +26c61439f947f01b412a70d7a7c9ccd2e1c03f28a8fc87b7459b67f3bd91da2d diff --git a/package-lock.json b/package-lock.json index 0249f51..b8b3675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "companygraph-meta-model": "github:companygraph/meta-model#v0.88.0" }, "devDependencies": { - "@robertblust/design": "github:robertblust/design#v0.145.0", + "@robertblust/design": "github:robertblust/design#v0.146.0", "companygraph-mcp-server": "github:companygraph/mcp-server#v0.62.0", "pdf-lib": "^1.17.1", "playwright": "^1.63.0", @@ -96,8 +96,8 @@ } }, "node_modules/@robertblust/design": { - "version": "0.145.0", - "resolved": "git+ssh://git@github.com/robertblust/design.git#3d3f68d9f1b4b69496a278ad645861ab314f5c24", + "version": "0.146.0", + "resolved": "git+ssh://git@github.com/robertblust/design.git#ef83b97e393e49b26904e4b99b997f7ab041cd1b", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 642b53c..268af97 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "pin:check": "node pin-check.mjs" }, "devDependencies": { - "@robertblust/design": "github:robertblust/design#v0.145.0", + "@robertblust/design": "github:robertblust/design#v0.146.0", "companygraph-mcp-server": "github:companygraph/mcp-server#v0.62.0", "pdf-lib": "^1.17.1", "playwright": "^1.63.0", diff --git a/principles/og.sha b/principles/og.sha index 5b6afd7..4c8fd55 100644 --- a/principles/og.sha +++ b/principles/og.sha @@ -1 +1 @@ -c228bbbb3f5414a986a6b0495036eeeabbec9f83ebe04b24f65458b0af53e8de +305bbef0aa0366e829db71a0fdd70089eabaedd39aeffe2b47d804aa24b1cc39 diff --git a/privacy/og.sha b/privacy/og.sha index d141de1..db4b586 100644 --- a/privacy/og.sha +++ b/privacy/og.sha @@ -1 +1 @@ -2b078486c35f2881c64827627f875983294b14a878347ca8161406273c192dd1 +f6d7a5f2d275ad749f91ca7bc4493064bf506e76b4b6f5137e5d02205ef42c0d diff --git a/problems/og.sha b/problems/og.sha index 19d54ed..ef2054a 100644 --- a/problems/og.sha +++ b/problems/og.sha @@ -1 +1 @@ -ab3139faa678190dafce7b5143b9a12c0b93d84c13c9b4c99539870e4e4ebda5 +6d3e78d7f4c73c4cd538f3c0cc5e45c241d3c2394e1cfb30c4a824e77e5052f1 diff --git a/processes/og.sha b/processes/og.sha index 95169d2..08e0f34 100644 --- a/processes/og.sha +++ b/processes/og.sha @@ -1 +1 @@ -6ecf489951e320d0d9f4b128856b1797f50b207b06989c767162ac60f7826ea9 +7a0f3c3f1dd453f6028d2b9628558be9599ae1ad10a305044a56589d64f5c09a diff --git a/processes/pictures/answering.sha b/processes/pictures/answering.sha index aba65ae..a9b9aff 100644 --- a/processes/pictures/answering.sha +++ b/processes/pictures/answering.sha @@ -1 +1 @@ -6d35268c4c26bc976996daf05d38a9a2719ff818c9a45a650c45afe41ef5d1b9 +36c28e3a9af3515c5be560e9624c91141c16e421bd306706ba20317deedc0d48 diff --git a/processes/pictures/contribution.sha b/processes/pictures/contribution.sha index d7d59f3..b133601 100644 --- a/processes/pictures/contribution.sha +++ b/processes/pictures/contribution.sha @@ -1 +1 @@ -275922c70f1ad840b00c68a052424da4a756b0e1e8728a3cacdf6d756c503ab0 +39fbe2b63466e47740d5a0b18e0e7539b0ca1c1b772697a597a6e4dce4a052fa diff --git a/processes/pictures/deciding.sha b/processes/pictures/deciding.sha index 16ce854..027e7c3 100644 --- a/processes/pictures/deciding.sha +++ b/processes/pictures/deciding.sha @@ -1 +1 @@ -2681eacfee8f399c68cc09b95fc710b24c6aa6fb311b9b45f8137ec0717bf459 +38fbc888caadf93f21afae94897af7ae56985f3c40064fde31ed04c7e282af06 diff --git a/processes/pictures/delivery.sha b/processes/pictures/delivery.sha index e6ac817..b8680ee 100644 --- a/processes/pictures/delivery.sha +++ b/processes/pictures/delivery.sha @@ -1 +1 @@ -618264e967daf54b8021630340c8a456e45241df22595fdfb49e4b4ab3a30b9a +cd93b530078ab085dec3fd07ca7d4da66ab9d5bf81fa551353840562c7e1433d diff --git a/processes/pictures/feature-request.sha b/processes/pictures/feature-request.sha index 7087ebf..faf32d7 100644 --- a/processes/pictures/feature-request.sha +++ b/processes/pictures/feature-request.sha @@ -1 +1 @@ -032e9c38a39c65c872a8d4659cacd7ec1a3d0ea7ce10accdc7412ab2c04aa264 +69b2ec4eec4dc11171187c708c60f0c447e3434b7548d80c8ebeab6a7425cef4 diff --git a/processes/pictures/narrating.sha b/processes/pictures/narrating.sha index 925708c..8ed97bd 100644 --- a/processes/pictures/narrating.sha +++ b/processes/pictures/narrating.sha @@ -1 +1 @@ -1a02fd9600d268e1cf43aba6631ae8c8d9cc0cd806334c4aed167628e26d65e0 +048e6e882f2082f7259c87825cd3f7a5a4c78c12e53c9866f75069d6d0119b24 diff --git a/surfaces/og.sha b/surfaces/og.sha index f4f2fb1..e1f0daf 100644 --- a/surfaces/og.sha +++ b/surfaces/og.sha @@ -1 +1 @@ -a62bece10e68732b3c4adec3b0312f2a9f1ea25de6999106e4bda41c55cea110 +3e1f1dd4434aab3436b5e62cf8546bcc2902f86d25109679f09f9c5986d1949f diff --git a/talks/intro/og.sha b/talks/intro/og.sha index ee6e5b5..4ff79aa 100644 --- a/talks/intro/og.sha +++ b/talks/intro/og.sha @@ -1 +1 @@ -7b6272bbf733b8be32b7177fcfbe7ce85cdcfb67fac453d2cf793c0f9588b270 +ce0fc93d958f918cffb3e9bc490bee4a4f2804d5d88d350e8036e71bb46fb8ac diff --git a/talks/og.sha b/talks/og.sha index 76d8348..951837d 100644 --- a/talks/og.sha +++ b/talks/og.sha @@ -1 +1 @@ -a08ac12412c0559d898882814c184768bb410e1ea94c70f11b381ed8a27ae3ee +4a8276464058b68abd8dad5826ab61fdc61646b210ce507fe5841c249ad03a3e