AI bridge: fix cross-realm reply drop + add diagnostics
read_editor_buffer came back empty in testing: the reply to the chat iframe was dropped because event.source (the iframe's Window) is from another JS realm, so dyn_into::<Window>() returned None. - reply via unchecked_ref::<Window>() (postMessage works cross-realm even though instanceof Window does not), plus a DOM-query fallback that posts straight to the '#ai-assistant-editor-embed iframe' contentWindow. - console.log at each step ([cmms-ai-bridge] …); on-page command-line error if save_to_bytes fails. - Cargo.toml: web-sys HtmlIFrameElement.
This commit is contained in:
parent
8908a20902
commit
89e014afa1
3 changed files with 72 additions and 35 deletions
|
|
@ -92,6 +92,7 @@ web-sys = { version = "=0.3.85", features = [
|
|||
"Document",
|
||||
"Element",
|
||||
"HtmlAnchorElement",
|
||||
"HtmlIFrameElement",
|
||||
"Blob",
|
||||
"File",
|
||||
"FileSystemDirectoryHandle",
|
||||
|
|
|
|||
|
|
@ -877,19 +877,26 @@ impl OpenCADStudio {
|
|||
// Reply with the active drawing as ASCII DXF (the editor's
|
||||
// export format regardless of the on-disk format). An empty
|
||||
// reply tells the CMMS read tool the buffer isn't ready.
|
||||
let dxf = self
|
||||
.tabs
|
||||
.get(self.active_tab)
|
||||
.and_then(|tab| {
|
||||
let dxf = match self.tabs.get(self.active_tab).map(|tab| {
|
||||
crate::io::save_to_bytes(
|
||||
&tab.scene.document,
|
||||
"dxf",
|
||||
tab.scene.document.version,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
|
||||
.unwrap_or_default();
|
||||
}) {
|
||||
Some(Ok(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
|
||||
Some(Err(e)) => {
|
||||
self.command_line.push_error(
|
||||
crate::tf!("AI Assistant: could not read the drawing: {e}").as_ref(),
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
None => String::new(),
|
||||
};
|
||||
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
|
||||
"[cmms-ai-bridge] poll: want_read, replying {} bytes DXF",
|
||||
dxf.len()
|
||||
)));
|
||||
crate::sys::reply_ai_editor_buffer(&dxf);
|
||||
}
|
||||
if let Some(dxf) = apply {
|
||||
|
|
|
|||
45
src/sys.rs
45
src/sys.rs
|
|
@ -560,7 +560,11 @@ mod ai_editor_bridge {
|
|||
|
||||
struct State {
|
||||
listener: Option<Closure<dyn FnMut(web_sys::MessageEvent)>>,
|
||||
reply_to: Option<web_sys::Window>,
|
||||
// The chat iframe's Window, kept as a raw JsValue: it comes from
|
||||
// another realm (the <iframe>), so `instanceof Window` / `dyn_into`
|
||||
// would fail - we only ever call `.postMessage()` on it, which works
|
||||
// cross-realm.
|
||||
reply_to: Option<JsValue>,
|
||||
want_read: bool,
|
||||
pending_apply: Option<String>,
|
||||
}
|
||||
|
|
@ -574,6 +578,10 @@ mod ai_editor_bridge {
|
|||
}) };
|
||||
}
|
||||
|
||||
fn log(msg: &str) {
|
||||
web_sys::console::log_1(&JsValue::from_str(&format!("[cmms-ai-bridge] {msg}")));
|
||||
}
|
||||
|
||||
fn str_field(obj: &JsValue, key: &str) -> Option<String> {
|
||||
js_sys::Reflect::get(obj, &JsValue::from_str(key))
|
||||
.ok()?
|
||||
|
|
@ -601,7 +609,8 @@ mod ai_editor_bridge {
|
|||
&JsValue::from_str("source"),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|s| s.dyn_into::<web_sys::Window>().ok());
|
||||
.filter(|s| !s.is_null() && !s.is_undefined());
|
||||
log(&format!("get-buffer received (source={})", source.is_some()));
|
||||
STATE.with(|state| {
|
||||
let mut state = state.borrow_mut();
|
||||
state.reply_to = source;
|
||||
|
|
@ -613,6 +622,7 @@ mod ai_editor_bridge {
|
|||
return;
|
||||
}
|
||||
if let Some(new_source) = str_field(&data, "new_source") {
|
||||
log(&format!("apply received ({} bytes)", new_source.len()));
|
||||
STATE.with(|state| {
|
||||
state.borrow_mut().pending_apply = Some(new_source);
|
||||
});
|
||||
|
|
@ -625,6 +635,7 @@ mod ai_editor_bridge {
|
|||
.add_event_listener_with_callback("message", cb.as_ref().unchecked_ref());
|
||||
state.listener = Some(cb);
|
||||
});
|
||||
log("message listener installed");
|
||||
}
|
||||
|
||||
/// Drain any queued bridge requests: `(a read was asked for, a buffer to apply)`.
|
||||
|
|
@ -639,11 +650,9 @@ mod ai_editor_bridge {
|
|||
}
|
||||
|
||||
/// Post the current drawing (ASCII DXF) back to the chat iframe that asked.
|
||||
/// Also broadcasts to the embedded chat iframe found in the DOM, in case
|
||||
/// `event.source` was unusable.
|
||||
pub fn reply_buffer(dxf: &str) {
|
||||
STATE.with(|state| {
|
||||
let Some(target) = state.borrow_mut().reply_to.take() else {
|
||||
return;
|
||||
};
|
||||
let message = js_sys::Object::new();
|
||||
let _ = js_sys::Reflect::set(
|
||||
&message,
|
||||
|
|
@ -658,8 +667,28 @@ mod ai_editor_bridge {
|
|||
let origin = web_sys::window()
|
||||
.and_then(|w| w.location().origin().ok())
|
||||
.unwrap_or_else(|| "*".to_string());
|
||||
let _ = target.post_message(&message, &origin);
|
||||
});
|
||||
|
||||
let mut posted = false;
|
||||
if let Some(target) = STATE.with(|state| state.borrow_mut().reply_to.take()) {
|
||||
if target
|
||||
.unchecked_ref::<web_sys::Window>()
|
||||
.post_message(&message, &origin)
|
||||
.is_ok()
|
||||
{
|
||||
posted = true;
|
||||
}
|
||||
}
|
||||
// Fallback: post straight to the embedded chat iframe's contentWindow.
|
||||
if let Some(frame_win) = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| d.query_selector("#ai-assistant-editor-embed iframe").ok().flatten())
|
||||
.and_then(|el| el.dyn_into::<web_sys::HtmlIFrameElement>().ok())
|
||||
.and_then(|f| f.content_window())
|
||||
{
|
||||
let _ = frame_win.post_message(&message, &origin);
|
||||
posted = true;
|
||||
}
|
||||
log(&format!("reply_buffer: {} bytes, delivered={}", dxf.len(), posted));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue