oxedyne/daimond/src/wasm/ask.rs
4.7 KiB, 1 run
created by r2519314175:973, 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 question card's edge — a thin binding to the JS driver `window.DaimondAsk`. |
| 2 | //! |
| 3 | //! WHY THIS MODULE EXISTS, WHICH IS NOT WHAT IT DOES. Of the seven things the owner types at a |
| 4 | //! session every day, six could be typed at Daimond and one could not: a model had no way to put |
| 5 | //! a decision to him with options he could tap. It could write a question in prose and stop, |
| 6 | //! which is a question answered by TYPING — and a decision answered by typing is a decision put |
| 7 | //! off. [`crate::tools::Tool::FileShow`]'s rule again, arriving at the most-used interaction in |
| 8 | //! a working session rather than at a file: a thing a person does every day that the model has no |
| 9 | //! way to reach is a thing the model will eventually deny is possible. |
| 10 | //! |
| 11 | //! **THE DRIVER IS HANDED A QUESTION AND HANDS BACK NOTHING BUT WHETHER IT DREW ONE.** It does |
| 12 | //! not wait for the answer and there is no promise on this side holding a turn open. That is the |
| 13 | //! whole difference between this and `parkConsent`, which parks a worker's consent question on the |
| 14 | //! Pending panel and holds the `resolve` of a promise in memory: a promise does not survive a |
| 15 | //! reload, so those tiles are marked expired when the page comes back and the tick is disabled. |
| 16 | //! A question here is drawn from the tool call's own arguments, exactly as `renderSaid` draws a |
| 17 | //! stored fold, so a reload redraws it with working buttons — because answering is only sending a |
| 18 | //! message, and a message needs nothing left over from the turn that asked. |
| 19 | //! |
| 20 | //! What comes back is therefore one fact: was a card drawn. A page with no driver in it must |
| 21 | //! fail loudly rather than let the model report a question nobody can see. |
| 22 | |
| 23 | use crate::llm::extract_json_bool; |
| 24 | use crate::wasm::js_str; |
| 25 | |
| 26 | use oxedyne_fe2o3_core::prelude::*; |
| 27 | |
| 28 | use wasm_bindgen::prelude::wasm_bindgen; |
| 29 | use wasm_bindgen::{JsCast, JsValue}; |
| 30 | use wasm_bindgen_futures::JsFuture; |
| 31 | |
| 32 | |
| 33 | #[wasm_bindgen] |
| 34 | extern "C" { |
| 35 | |
| 36 | /// The driver object `www/js/daimond.js` installs at `window.DaimondAsk`. |
| 37 | #[wasm_bindgen(js_name = DaimondAsk)] |
| 38 | type Asker; |
| 39 | |
| 40 | /// Draw one question in the conversation, and answer whether it was drawn. |
| 41 | #[wasm_bindgen(method)] |
| 42 | fn put(this: &Asker, payload: &str) -> js_sys::Promise; |
| 43 | } |
| 44 | |
| 45 | |
| 46 | /// Reach the driver object on `window`, or refuse in the model's language. |
| 47 | fn asker() -> Outcome<Asker> { |
| 48 | let win = res!(web_sys::window() |
| 49 | .ok_or_else(|| err!("Putting a question needs a browser window."; System, Missing))); |
| 50 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondAsk")) |
| 51 | .map_err(|e| err!("Reading window.DaimondAsk failed: {}.", js_str(&e); System, Missing))); |
| 52 | if obj.is_undefined() || obj.is_null() { |
| 53 | return Err(err!( |
| 54 | "This page cannot draw a question card, so nobody would see one. Ask in prose \ |
| 55 | instead, giving the options, your recommendation and the reason for it."; |
| 56 | System, Missing)); |
| 57 | } |
| 58 | Ok(obj.unchecked_into::<Asker>()) |
| 59 | } |
| 60 | |
| 61 | /// The `message` of a rejected JS `Error`, verbatim. A refusal from the driver is written for |
| 62 | /// the model to read and act on, so nothing here rewords it. |
| 63 | fn refusal(e: &JsValue) -> String { |
| 64 | match js_sys::Reflect::get(e, &JsValue::from_str("message")) { |
| 65 | Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)), |
| 66 | Err(_) => js_str(e), |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | /// Put one question on the screen, and say nothing about the answer. |
| 71 | /// |
| 72 | /// # Arguments |
| 73 | /// * `payload` - The checked question as `ask_step` composed it, as JSON. |
| 74 | pub async fn put(payload: &str) -> Outcome<()> { |
| 75 | let a = res!(asker()); |
| 76 | let v = match JsFuture::from(a.put(payload)).await { |
| 77 | Ok(v) => v, |
| 78 | Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)), |
| 79 | }; |
| 80 | let json = match v.as_string() { |
| 81 | Some(s) => s, |
| 82 | None => match js_sys::JSON::stringify(&v) { |
| 83 | Ok(s) => String::from(s), |
| 84 | Err(_) => return Err(err!( |
| 85 | "The page answered with something that cannot be read, so whether the user can \ |
| 86 | see the question is unknown. Do not tell them you have asked."; Invalid, Data)), |
| 87 | }, |
| 88 | }; |
| 89 | // Absent means NOT drawn, which is the opposite of `doc::show`'s reading of a missing field |
| 90 | // and deliberately so: a show that predates its own `shown` field really did show something, |
| 91 | // while a page that says nothing here has said nothing about a card the model is about to |
| 92 | // claim is on screen. |
| 93 | if !extract_json_bool(&json, "drawn").unwrap_or(false) { |
| 94 | return Err(err!( |
| 95 | "The page did not draw the question, so there is nothing on screen to tap. Ask in \ |
| 96 | prose instead, giving the options, your recommendation and the reason for it."; |
| 97 | IO, Invalid)); |
| 98 | } |
| 99 | Ok(()) |
| 100 | } |