Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/src/wasm/doc.rs

6.5 KiB, 1 run

created by r2519314175:979, 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 document panel's edge — a thin binding to the JS driver `window.DaimondDoc`.
2//!
3//! WHY THIS MODULE EXISTS, WHICH IS NOT THE SAME AS WHAT IT DOES. Asked to compile a Typst
4//! source and display the PDF, a daimon answered:
5//!
6//! > I do see a pre-existing thinking.pdf (1.08 MB) ... but I cannot display a PDF inline here
7//! > — the file tools return raw bytes for it rather than a rendered view.
8//!
9//! Every clause of that is true about its TOOLBOX and false about Daimond. `www/js/viewer.js`
10//! has handed `application/pdf` to the browser's own document viewer since the `doc` tier was
11//! written, with the security argument for doing so measured and recorded beside it. What was
12//! missing was a way for the model to SAY SO: it held eleven tools that return bytes and none
13//! that put a file in front of a person, so it reasoned from the toolbox to a limitation of the
14//! app, reported that limitation to the user, and spent the turn apologising for it.
15//!
16//! A working surface the model cannot reach is a surface the model will deny.
17//!
18//! **The driver is handed a PATH and never bytes.** A view given content is a snapshot: when the
19//! file is written again -- a recompile, a rebuild -- there is nowhere for the new bytes to land,
20//! and the only way to refresh it is to build a second view. A view that names a workspace file
21//! reads that file whenever it is drawn, so showing the same path again is the whole of how a
22//! rebuilt document reaches the reader.
23//!
24//! The driver resolves with `viewer.js`'s own verdict about what it drew. Composing that
25//! sentence from a table here instead would put a second opinion about which formats have a
26//! viewer into a second language, and the two would disagree the first time a format moved tier.
27
28use crate::llm::{
29 extract_json_bool,
30 extract_json_number,
31 extract_json_string,
32};
33use crate::tools::Shown;
34use crate::wasm::js_str;
35
36use oxedyne_fe2o3_core::prelude::*;
37
38use wasm_bindgen::prelude::wasm_bindgen;
39use wasm_bindgen::{JsCast, JsValue};
40use wasm_bindgen_futures::JsFuture;
41
42
43#[wasm_bindgen]
44extern "C" {
45
46 /// The driver object `www/js/viewer.js` installs at `window.DaimondDoc`.
47 #[wasm_bindgen(js_name = DaimondDoc)]
48 type Panel;
49
50 /// Put a workspace file in the document panel, at `page` for a paged format.
51 ///
52 /// `page` is a `JsValue` so it can be `undefined`, which is the driver's word for "the top"
53 /// and is not the same as page 1 asked for deliberately.
54 ///
55 /// `owner` names the conversation asking, so the driver can decline to take a screen that
56 /// belongs to another one. An empty string means the caller could not say, and the driver
57 /// reads that as the user's own act.
58 #[wasm_bindgen(method)]
59 fn show(this: &Panel, path: &str, page: JsValue, owner: &str) -> js_sys::Promise;
60}
61
62
63/// Reach the driver object on `window`, or refuse in the model's language.
64fn panel() -> Outcome<Panel> {
65 let win = res!(web_sys::window()
66 .ok_or_else(|| err!("Showing a file needs a browser window."; System, Missing)));
67 let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondDoc"))
68 .map_err(|e| err!("Reading window.DaimondDoc failed: {}.", js_str(&e); System, Missing)));
69 if obj.is_undefined() || obj.is_null() {
70 return Err(err!(
71 "Daimond's document panel is not loaded in this page, so there is nothing to show a \
72 file in. Tell the user, and describe what you would have shown them instead.";
73 System, Missing));
74 }
75 Ok(obj.unchecked_into::<Panel>())
76}
77
78/// The `message` of a rejected JS `Error`, verbatim. A refusal from the driver is written for
79/// the model to read and act on, so nothing here rewords it.
80fn refusal(e: &JsValue) -> String {
81 match js_sys::Reflect::get(e, &JsValue::from_str("message")) {
82 Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)),
83 Err(_) => js_str(e),
84 }
85}
86
87/// Show `path` in the document panel, and report what the user is now looking at.
88///
89/// # Arguments
90/// * `path` - The workspace-relative path, already scoped and already checked by the guard.
91/// * `page` - Which page to open a paged document at, or `None` for the top.
92/// * `owner` - The Diamond whose daimon is asking, or empty for a chat or the user.
93pub async fn show(path: &str, page: Option<u32>, owner: &str) -> Outcome<Shown> {
94 let p = res!(panel());
95 let at = match page {
96 Some(n) => JsValue::from_f64(n as f64),
97 None => JsValue::UNDEFINED,
98 };
99 let v = match JsFuture::from(p.show(path, at, owner)).await {
100 Ok(v) => v,
101 Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)),
102 };
103 let json = match v.as_string() {
104 Some(s) => s,
105 None => match js_sys::JSON::stringify(&v) {
106 Ok(s) => String::from(s),
107 Err(_) => return Err(err!(
108 "The document panel answered with something that cannot be read, so what it drew \
109 is unknown. Do not tell the user what they are looking at."; Invalid, Data)),
110 },
111 };
112 // A verdict with no tier in it is not a verdict. Reporting it as a successful show would have
113 // the model describe a screen nothing has said anything about.
114 let tier = match extract_json_string(&json, "tier") {
115 Some(t) if !t.trim().is_empty() => t,
116 _ => return Err(err!(
117 "The document panel did not say what it drew, so what the user is looking at is \
118 unknown."; Invalid, Data)),
119 };
120 Ok(Shown {
121 tier,
122 media: extract_json_string(&json, "media").unwrap_or_default(),
123 label: extract_json_string(&json, "label").unwrap_or_default(),
124 size: extract_json_number(&json, "size").unwrap_or(0),
125 // The panel's answer and not the argument above: a show with no page named leaves a
126 // document where it was last aimed, so these two differ on every re-show.
127 page: match extract_json_number(&json, "page") {
128 Some(n) if n > 0 => Some(n as u32),
129 _ => None,
130 },
131 disagree: extract_json_bool(&json, "disagree").unwrap_or(false),
132 named: extract_json_string(&json, "named").unwrap_or_default(),
133 found: extract_json_string(&json, "found").unwrap_or_default(),
134 // Absent means shown. An older bundle's driver answers no `shown` at all, and the tool
135 // predates the field: reading a missing one as "not shown" would have every show in that
136 // bundle report a failure that did not happen.
137 shown: extract_json_bool(&json, "shown").unwrap_or(true),
138 })
139}