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",
|
"Document",
|
||||||
"Element",
|
"Element",
|
||||||
"HtmlAnchorElement",
|
"HtmlAnchorElement",
|
||||||
|
"HtmlIFrameElement",
|
||||||
"Blob",
|
"Blob",
|
||||||
"File",
|
"File",
|
||||||
"FileSystemDirectoryHandle",
|
"FileSystemDirectoryHandle",
|
||||||
|
|
|
||||||
|
|
@ -877,19 +877,26 @@ impl OpenCADStudio {
|
||||||
// Reply with the active drawing as ASCII DXF (the editor's
|
// Reply with the active drawing as ASCII DXF (the editor's
|
||||||
// export format regardless of the on-disk format). An empty
|
// export format regardless of the on-disk format). An empty
|
||||||
// reply tells the CMMS read tool the buffer isn't ready.
|
// reply tells the CMMS read tool the buffer isn't ready.
|
||||||
let dxf = self
|
let dxf = match self.tabs.get(self.active_tab).map(|tab| {
|
||||||
.tabs
|
crate::io::save_to_bytes(
|
||||||
.get(self.active_tab)
|
&tab.scene.document,
|
||||||
.and_then(|tab| {
|
"dxf",
|
||||||
crate::io::save_to_bytes(
|
tab.scene.document.version,
|
||||||
&tab.scene.document,
|
)
|
||||||
"dxf",
|
}) {
|
||||||
tab.scene.document.version,
|
Some(Ok(bytes)) => String::from_utf8_lossy(&bytes).into_owned(),
|
||||||
)
|
Some(Err(e)) => {
|
||||||
.ok()
|
self.command_line.push_error(
|
||||||
})
|
crate::tf!("AI Assistant: could not read the drawing: {e}").as_ref(),
|
||||||
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
|
);
|
||||||
.unwrap_or_default();
|
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);
|
crate::sys::reply_ai_editor_buffer(&dxf);
|
||||||
}
|
}
|
||||||
if let Some(dxf) = apply {
|
if let Some(dxf) = apply {
|
||||||
|
|
|
||||||
73
src/sys.rs
73
src/sys.rs
|
|
@ -560,7 +560,11 @@ mod ai_editor_bridge {
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
listener: Option<Closure<dyn FnMut(web_sys::MessageEvent)>>,
|
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,
|
want_read: bool,
|
||||||
pending_apply: Option<String>,
|
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> {
|
fn str_field(obj: &JsValue, key: &str) -> Option<String> {
|
||||||
js_sys::Reflect::get(obj, &JsValue::from_str(key))
|
js_sys::Reflect::get(obj, &JsValue::from_str(key))
|
||||||
.ok()?
|
.ok()?
|
||||||
|
|
@ -601,7 +609,8 @@ mod ai_editor_bridge {
|
||||||
&JsValue::from_str("source"),
|
&JsValue::from_str("source"),
|
||||||
)
|
)
|
||||||
.ok()
|
.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| {
|
STATE.with(|state| {
|
||||||
let mut state = state.borrow_mut();
|
let mut state = state.borrow_mut();
|
||||||
state.reply_to = source;
|
state.reply_to = source;
|
||||||
|
|
@ -613,6 +622,7 @@ mod ai_editor_bridge {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(new_source) = str_field(&data, "new_source") {
|
if let Some(new_source) = str_field(&data, "new_source") {
|
||||||
|
log(&format!("apply received ({} bytes)", new_source.len()));
|
||||||
STATE.with(|state| {
|
STATE.with(|state| {
|
||||||
state.borrow_mut().pending_apply = Some(new_source);
|
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());
|
.add_event_listener_with_callback("message", cb.as_ref().unchecked_ref());
|
||||||
state.listener = Some(cb);
|
state.listener = Some(cb);
|
||||||
});
|
});
|
||||||
|
log("message listener installed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain any queued bridge requests: `(a read was asked for, a buffer to apply)`.
|
/// Drain any queued bridge requests: `(a read was asked for, a buffer to apply)`.
|
||||||
|
|
@ -639,27 +650,45 @@ mod ai_editor_bridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post the current drawing (ASCII DXF) back to the chat iframe that asked.
|
/// 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) {
|
pub fn reply_buffer(dxf: &str) {
|
||||||
STATE.with(|state| {
|
let message = js_sys::Object::new();
|
||||||
let Some(target) = state.borrow_mut().reply_to.take() else {
|
let _ = js_sys::Reflect::set(
|
||||||
return;
|
&message,
|
||||||
};
|
&JsValue::from_str("type"),
|
||||||
let message = js_sys::Object::new();
|
&JsValue::from_str("cmms-ai-editor-buffer"),
|
||||||
let _ = js_sys::Reflect::set(
|
);
|
||||||
&message,
|
let _ = js_sys::Reflect::set(
|
||||||
&JsValue::from_str("type"),
|
&message,
|
||||||
&JsValue::from_str("cmms-ai-editor-buffer"),
|
&JsValue::from_str("buffer"),
|
||||||
);
|
&JsValue::from_str(dxf),
|
||||||
let _ = js_sys::Reflect::set(
|
);
|
||||||
&message,
|
let origin = web_sys::window()
|
||||||
&JsValue::from_str("buffer"),
|
.and_then(|w| w.location().origin().ok())
|
||||||
&JsValue::from_str(dxf),
|
.unwrap_or_else(|| "*".to_string());
|
||||||
);
|
|
||||||
let origin = web_sys::window()
|
let mut posted = false;
|
||||||
.and_then(|w| w.location().origin().ok())
|
if let Some(target) = STATE.with(|state| state.borrow_mut().reply_to.take()) {
|
||||||
.unwrap_or_else(|| "*".to_string());
|
if target
|
||||||
let _ = target.post_message(&message, &origin);
|
.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