oxedyne/daimond/src/wasm/social.rs
7.3 KiB, 1 run
created by r2519314175:995, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The Social panel's edge — a thin binding to the JS driver `window.DaimondSocial`. |
| 2 | //! |
| 3 | //! WHY THIS MODULE EXISTS, WHICH IS NOT WHAT IT DOES. On 2026-08-24 two real daimons, on two |
| 4 | //! accounts and two different models, were each asked to do one side of the Social panel's work. |
| 5 | //! Neither could reach it, and neither said so: one told its user to go and find "Daimond's |
| 6 | //! feedback/issue reporting interface (typically accessible from a menu in the app)", which is |
| 7 | //! the panel it had just failed to find, and the other spent eighteen calls and finished with |
| 8 | //! "The gateway isn't running." The gateway was running. The rule they broke is |
| 9 | //! [`crate::tools::Tool::FileShow`]'s, and it is stated there as a defect class rather than a |
| 10 | //! feature: a working surface the model cannot reach is a surface the model will deny. |
| 11 | //! |
| 12 | //! **THE PANEL COMPOSES THE PROSE ABOUT RECORDS AND THIS SIDE COMPOSES THE POLICY.** Which |
| 13 | //! proposals exist, what state each is in, what its tally is and what a note's sealed build |
| 14 | //! identifier is are all the panel's answers, drawn on a screen the user is looking at; a second |
| 15 | //! renderer here would be a second opinion in a second language, disagreeing with that screen the |
| 16 | //! first time either changed. It is the same argument [`crate::tools::Shown`] is written under. |
| 17 | //! What Rust keeps is every refusal and the consent question, because those are the sentences |
| 18 | //! that decide something. |
| 19 | //! |
| 20 | //! **CONSENT IS BOUND TO BYTES AND NOT TO ARGUMENTS**, which is why publishing is two calls and |
| 21 | //! not one. [`compose`] asks the panel what would actually leave and hands back both the |
| 22 | //! characters the user is shown and an opaque token standing for them; [`commit`] sends what that |
| 23 | //! token holds. Composing once and sending the arguments again afterwards would mean the user |
| 24 | //! approved a rendering and the app sent a rebuild of it, and the two part company at exactly the |
| 25 | //! field a person would want to have seen -- the build identifier that travels with a note, the |
| 26 | //! title of the proposal a vote lands on. |
| 27 | |
| 28 | use crate::llm::extract_json_string; |
| 29 | use crate::wasm::js_str; |
| 30 | |
| 31 | use oxedyne_fe2o3_core::prelude::*; |
| 32 | |
| 33 | use wasm_bindgen::prelude::wasm_bindgen; |
| 34 | use wasm_bindgen::{JsCast, JsValue}; |
| 35 | use wasm_bindgen_futures::JsFuture; |
| 36 | |
| 37 | |
| 38 | #[wasm_bindgen] |
| 39 | extern "C" { |
| 40 | |
| 41 | /// The driver object `www/js/improve.js` installs at `window.DaimondSocial`. |
| 42 | #[wasm_bindgen(js_name = DaimondSocial)] |
| 43 | type Panel; |
| 44 | |
| 45 | /// Read one view of the panel, and answer with the text a model reads. |
| 46 | #[wasm_bindgen(method)] |
| 47 | fn read(this: &Panel, req: &str) -> js_sys::Promise; |
| 48 | |
| 49 | /// Work out exactly what one act would put on the wire, without sending any of it. |
| 50 | #[wasm_bindgen(method)] |
| 51 | fn compose(this: &Panel, req: &str) -> js_sys::Promise; |
| 52 | |
| 53 | /// Send what a `compose` composed, named by the token it minted for it. |
| 54 | #[wasm_bindgen(method)] |
| 55 | fn commit(this: &Panel, token: &str) -> js_sys::Promise; |
| 56 | } |
| 57 | |
| 58 | |
| 59 | /// What came back from asking the panel what one act would publish. |
| 60 | /// |
| 61 | /// A refusal is an ANSWER and not an error. Every reason a compose can fail -- no voice on the |
| 62 | /// forge, a proposal number that is not there, an act this build does not have -- is something |
| 63 | /// the model can act on and must be told in its own language, so it comes back as a sentence to |
| 64 | /// hand over rather than as an `Err` the dispatcher would dress in a failure line. |
| 65 | #[derive(Clone, Debug)] |
| 66 | pub enum Composed { |
| 67 | // Ready to put to the user. `token` is opaque on this side on purpose: Rust must not be |
| 68 | // able to assemble one, or the binding between what was seen and what is sent is a |
| 69 | // convention rather than a mechanism. |
| 70 | Draft { |
| 71 | shown: String, // exactly what the user is shown |
| 72 | token: String, // the panel's handle on the payload those characters describe |
| 73 | }, |
| 74 | Refused(String), // nothing was composed, and this is what the model reads instead |
| 75 | } |
| 76 | |
| 77 | /// Reach the driver object on `window`, or refuse in the model's language. |
| 78 | fn panel() -> Outcome<Panel> { |
| 79 | let win = res!(web_sys::window() |
| 80 | .ok_or_else(|| err!("Reaching the Social panel needs a browser window."; System, Missing))); |
| 81 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondSocial")) |
| 82 | .map_err(|e| err!("Reading window.DaimondSocial failed: {}.", js_str(&e); System, Missing))); |
| 83 | if obj.is_undefined() || obj.is_null() { |
| 84 | return Err(err!( |
| 85 | "Daimond's Social panel is not loaded in this page. Tell the user what you wanted to \ |
| 86 | read or publish there, and carry on without it."; System, Missing)); |
| 87 | } |
| 88 | Ok(obj.unchecked_into::<Panel>()) |
| 89 | } |
| 90 | |
| 91 | /// The `message` of a rejected JS `Error`, verbatim. A refusal from the driver is written for |
| 92 | /// the model to read and act on, so nothing here rewords it. |
| 93 | fn refusal(e: &JsValue) -> String { |
| 94 | match js_sys::Reflect::get(e, &JsValue::from_str("message")) { |
| 95 | Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)), |
| 96 | Err(_) => js_str(e), |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// Settle a driver promise that answers with a string. |
| 101 | async fn text(p: js_sys::Promise) -> Outcome<String> { |
| 102 | let v = match JsFuture::from(p).await { |
| 103 | Ok(v) => v, |
| 104 | Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)), |
| 105 | }; |
| 106 | match v.as_string() { |
| 107 | Some(s) => Ok(s), |
| 108 | None => Err(err!( |
| 109 | "The Social panel answered with something that cannot be read, so what it did is \ |
| 110 | unknown. Do not tell the user it worked."; Invalid, Data)), |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// Read one view, and answer with what the panel says is on it. |
| 115 | /// |
| 116 | /// # Arguments |
| 117 | /// * `req` - The request [`crate::tools::social_read_step`] composed. |
| 118 | pub async fn read(req: &str) -> Outcome<String> { |
| 119 | let p = res!(panel()); |
| 120 | text(p.read(req)).await |
| 121 | } |
| 122 | |
| 123 | /// Ask what one act would publish, without publishing any of it. |
| 124 | /// |
| 125 | /// # Arguments |
| 126 | /// * `req` - The request [`crate::tools::social_send_step`] composed. |
| 127 | pub async fn compose(req: &str) -> Outcome<Composed> { |
| 128 | let p = res!(panel()); |
| 129 | let json = res!(text(p.compose(req)).await); |
| 130 | if let Some(no) = extract_json_string(&json, "refusal") { |
| 131 | if !no.trim().is_empty() { |
| 132 | return Ok(Composed::Refused(no)); |
| 133 | } |
| 134 | } |
| 135 | // Neither field may be empty and neither is optional. A draft with nothing to show would |
| 136 | // put an empty dialog in front of the user, who would then be approving a blank -- and a |
| 137 | // draft with no token would be a yes with nothing to spend it on. |
| 138 | let shown = match extract_json_string(&json, "shown") { |
| 139 | Some(s) if !s.trim().is_empty() => s, |
| 140 | _ => return Err(err!( |
| 141 | "The Social panel did not say what it would publish, so there is nothing to put to \ |
| 142 | the user. Nothing was sent."; Invalid, Data)), |
| 143 | }; |
| 144 | let token = match extract_json_string(&json, "token") { |
| 145 | Some(t) if !t.trim().is_empty() => t, |
| 146 | _ => return Err(err!( |
| 147 | "The Social panel composed something and would not name it, so there is no way to \ |
| 148 | send exactly what the user would have approved. Nothing was sent."; Invalid, Data)), |
| 149 | }; |
| 150 | Ok(Composed::Draft { shown, token }) |
| 151 | } |
| 152 | |
| 153 | /// Publish what a [`compose`] composed. |
| 154 | /// |
| 155 | /// # Arguments |
| 156 | /// * `token` - The handle the panel minted for that draft, and no other. |
| 157 | pub async fn commit(token: &str) -> Outcome<String> { |
| 158 | let p = res!(panel()); |
| 159 | text(p.commit(token)).await |
| 160 | } |