fix: scaling, and chat

* Chat ergonomics vastly improved.
* Scaling now done through client side settings
This commit is contained in:
Evan Carroll 2026-01-14 12:53:16 -06:00
parent 98f38c9714
commit b430c80000
8 changed files with 1564 additions and 439 deletions

View file

@ -134,7 +134,7 @@ fn SceneDetailView(
let (show_delete_confirm, set_show_delete_confirm) = signal(false);
let (show_image_modal, set_show_image_modal) = signal(false);
// Parse dimensions from bounds_wkt
// Parse dimensions from bounds_wkt for display
let (initial_width, initial_height) = parse_bounds_wkt(&scene.bounds_wkt);
// Clone scene data for view (to avoid move issues)
@ -145,7 +145,6 @@ fn SceneDetailView(
let scene_background_image_path = scene.background_image_path.clone();
let scene_background_image_path_for_modal = scene.background_image_path.clone();
let scene_background_image_path_for_check = scene.background_image_path.clone();
let scene_background_image_path_for_dimensions = scene.background_image_path.clone();
let scene_background_color_display = scene.background_color.clone();
let scene_created_at = scene.created_at.clone();
let scene_updated_at = scene.updated_at.clone();
@ -158,42 +157,32 @@ fn SceneDetailView(
);
let (background_image_url, set_background_image_url) = signal(String::new());
let (clear_background_image, set_clear_background_image) = signal(false);
let (infer_dimensions, set_infer_dimensions) = signal(false);
let (width, set_width) = signal(initial_width);
let (height, set_height) = signal(initial_height);
let (dimension_mode, set_dimension_mode) = signal(scene.dimension_mode.clone());
let (sort_order, set_sort_order) = signal(scene.sort_order);
let (is_entry_point, set_is_entry_point) = signal(scene.is_entry_point);
let (is_hidden, set_is_hidden) = signal(scene.is_hidden);
// UI state for dimension fetching
// UI state for dimension detection (read-only display)
let (fetching_dimensions, set_fetching_dimensions) = signal(false);
let (dimension_message, set_dimension_message) = signal(Option::<(String, bool)>::None);
let (detected_dimensions, set_detected_dimensions) = signal(Option::<(u32, u32)>::None);
let fetch_dimensions = move |_: leptos::ev::MouseEvent| {
let url = background_image_url.get();
if url.is_empty() {
set_dimension_message.set(Some(("Please enter an image URL first".to_string(), false)));
return;
}
set_fetching_dimensions.set(true);
set_dimension_message.set(None);
set_detected_dimensions.set(None);
#[cfg(feature = "hydrate")]
{
fetch_image_dimensions_client(
url,
move |w, h| {
set_width.set(w as i32);
set_height.set(h as i32);
set_dimension_message.set(Some((
format!("Dimensions: {}x{}", w, h),
true,
)));
set_detected_dimensions.set(Some((w, h)));
},
move |err| {
set_dimension_message.set(Some((err, false)));
move |_err| {
set_detected_dimensions.set(None);
},
set_fetching_dimensions,
);
@ -210,17 +199,11 @@ fn SceneDetailView(
use gloo_net::http::Request;
let id = scene_id.clone();
// Build bounds WKT from width/height
let w = width.get();
let h = height.get();
let bounds_wkt = format!("POLYGON((0 0, {} 0, {} {}, 0 {}, 0 0))", w, w, h, h);
let mut data = serde_json::json!({
"name": name.get(),
"description": if description.get().is_empty() { None::<String> } else { Some(description.get()) },
"background_color": if background_color.get().is_empty() { None::<String> } else { Some(background_color.get()) },
"bounds_wkt": bounds_wkt,
"dimension_mode": dimension_mode.get(),
"sort_order": sort_order.get(),
"is_entry_point": is_entry_point.get(),
"is_hidden": is_hidden.get()
@ -230,10 +213,8 @@ fn SceneDetailView(
let bg_url = background_image_url.get();
if !bg_url.is_empty() {
data["background_image_url"] = serde_json::json!(bg_url);
// Include infer dimensions flag when uploading new image
if infer_dimensions.get() {
data["infer_dimensions_from_image"] = serde_json::json!(true);
}
// Always infer dimensions from new image
data["infer_dimensions_from_image"] = serde_json::json!(true);
}
// Include clear flag if set
@ -447,35 +428,19 @@ fn SceneDetailView(
</div>
</Show>
// Dimension fetch message
<Show when=move || dimension_message.get().is_some()>
// Show detected dimensions (read-only feedback for new images)
<Show when=move || detected_dimensions.get().is_some()>
{move || {
let (msg, is_success) = dimension_message.get().unwrap_or_default();
let class = if is_success { "alert alert-success" } else { "alert alert-error" };
let (w, h) = detected_dimensions.get().unwrap_or((0, 0));
view! {
<div class=class role="alert" style="margin-bottom: 1rem">
<p>{msg}</p>
<div class="alert alert-info" role="status" style="margin-bottom: 1rem">
<p>"Detected Size: " <strong>{w}</strong> " × " <strong>{h}</strong> " px"</p>
<small>"Dimensions will be extracted from the new image when saved"</small>
</div>
}
}}
</Show>
// Infer dimensions checkbox (only shown when new URL is provided)
<Show when=move || !background_image_url.get().is_empty()>
<div class="checkbox-group">
<label class="checkbox-label">
<input
type="checkbox"
class="form-checkbox"
prop:checked=move || infer_dimensions.get()
on:change=move |ev| set_infer_dimensions.set(event_target_checked(&ev))
/>
"Infer dimensions from image"
</label>
<small class="form-help">"If enabled, server will extract dimensions from the image when saving"</small>
</div>
</Show>
{if scene_background_image_path_for_check.is_some() {
view! {
<div class="checkbox-group">
@ -494,97 +459,6 @@ fn SceneDetailView(
view! {}.into_any()
}}
<h3 class="section-title">"Dimensions"</h3>
<div class="form-row">
<div class="form-group">
<label for="width" class="form-label">"Width"</label>
<input
type="number"
id="width"
min=100
max=10000
class="form-input"
prop:value=move || width.get()
on:input=move |ev| {
if let Ok(v) = event_target_value(&ev).parse() {
set_width.set(v);
}
}
/>
</div>
<div class="form-group">
<label for="height" class="form-label">"Height"</label>
<input
type="number"
id="height"
min=100
max=10000
class="form-input"
prop:value=move || height.get()
on:input=move |ev| {
if let Ok(v) = event_target_value(&ev).parse() {
set_height.set(v);
}
}
/>
</div>
<div class="form-group">
<label for="dimension_mode" class="form-label">"Dimension Mode"</label>
<select
id="dimension_mode"
class="form-select"
on:change=move |ev| set_dimension_mode.set(event_target_value(&ev))
>
<option value="fixed" selected=move || dimension_mode.get() == "fixed">"Fixed"</option>
<option value="viewport" selected=move || dimension_mode.get() == "viewport">"Viewport"</option>
</select>
</div>
</div>
// Button to set dimensions from existing background image
{if let Some(ref path) = scene_background_image_path_for_dimensions {
let path_for_closure = path.clone();
view! {
<div class="form-group">
<button
type="button"
class="btn btn-secondary"
disabled=move || fetching_dimensions.get()
on:click=move |_| {
set_fetching_dimensions.set(true);
set_dimension_message.set(None);
#[cfg(feature = "hydrate")]
{
let path = path_for_closure.clone();
fetch_image_dimensions_client(
path,
move |w, h| {
set_width.set(w as i32);
set_height.set(h as i32);
set_dimension_message.set(Some((
format!("Set from image: {}x{}", w, h),
true,
)));
},
move |err| {
set_dimension_message.set(Some((err, false)));
},
set_fetching_dimensions,
);
}
}
>
{move || if fetching_dimensions.get() { "Fetching..." } else { "Set from background image" }}
</button>
<small class="form-help">"Set dimensions to match the current background image"</small>
</div>
}.into_any()
} else {
view! {}.into_any()
}}
<h3 class="section-title">"Options"</h3>
<div class="form-group">