//! The full joule-ui loop in one file: declare a typed surface with //! `joule-ui-core`, validate it against a registry, then fire an action on it //! through `joule-ui-interaction` so the invocation is **metered, budget-gated, //! and sealed into a hash-chained receipt**. The thing Gradio/Streamlit/A2UI //! cannot show: a UI whose every interaction is an auditable, energy-accounted //! transaction. //! //! ```bash //! cargo run -p joule-ui-interaction --example metered_surface //! ``` use joule_ui_core::{ AllowedChildren, PropSchema, PropType, PropValue, Registry, Widget, WidgetSchema, validate, }; use joule_ui_interaction::{ Authorizer, ContentId, Energy, InteractionLog, Meter, run_interaction, verify_chain, }; use std::cell::Cell; /// A wall-clock-free deterministic meter standing in for the Sandbox standard's /// `PowerMeter`. The point of the example is the *protocol*. It declares no /// hardware grade, so its figures are sealed as `Estimator`: a hardware /// figure is the Sandbox edge's to supply, and only from a live counter. struct DemoMeter(Cell); impl Meter for DemoMeter { fn sample_uj(&self) -> Option { let v = self.0.get(); self.0.set(v + 4200); // each sample advances 4.2 mJ Some(v) } } /// A joule budget — the JCP edge in miniature. struct Budget { remaining: u64, } impl Authorizer for Budget { fn authorize(&mut self, _action: &str) -> Result { if self.remaining > 0 { Ok(self.remaining) } else { Err("budget-exceeded".into()) } } fn charge(&mut self, _action: &str, cost: &Energy) -> u64 { self.remaining = self.remaining.saturating_sub(cost.uj()); self.remaining } fn grant_id(&self) -> Option { Some("did:web:demo#grant-1".into()) } } fn main() { // 1. Declare the surface: a Form holding a Slider whose change fires "apply". let mut registry = Registry::new(); registry.add( WidgetSchema::new("Form", "a vertical form").children(AllowedChildren::Any), ); registry.add( WidgetSchema::new("Slider", "a numeric slider") .prop("min", PropSchema::required(PropType::Number)) .prop("max", PropSchema::required(PropType::Number)) .prop("on_change", PropSchema::required(PropType::Action)) .children(AllowedChildren::None), ); registry.add(WidgetSchema::new("Image", "an image output").children(AllowedChildren::None)); let surface = Widget::leaf("Form").child( Widget::leaf("Slider") .prop("min", 1.0) .prop("max", 10.0) .prop( "on_change", PropValue::Action { id: "apply".into(), payload: None }, ), ); let errors = validate(&surface, ®istry); assert!(errors.is_empty(), "surface validates: {errors:?}"); let surface_id = ContentId::of_bytes(serde_json::to_string(&surface).unwrap().as_bytes()); println!("surface declared + validated; id = {}", surface_id.0); // 2. Fire three "apply" interactions (the user dragging the slider), each // metered, budget-gated, and sealed into the chain. let signing = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]); let mut log = InteractionLog::new(signing); let mut budget = Budget { remaining: 10_000 }; for intensity in [3u8, 6, 9] { let meter = DemoMeter(Cell::new(0)); let inputs = format!(r#"{{"intensity":{intensity}}}"#); let interaction = run_interaction( &surface_id, "apply", "did:key:z6MkDemoUser", inputs.as_bytes(), &meter, Some(&mut budget), || { // The handler: produce the output widget (a processed Image). let out = Widget::leaf("Image").prop("intensity", intensity as f64); serde_json::to_vec(&out).unwrap() }, ); println!( " apply(intensity={intensity}): {} · {} µJ [{}] · budget {} µJ left", interaction.verdict.tag(), interaction.energy.uj(), interaction.energy.provenance.label(), interaction.budget_remaining_uj.unwrap_or(0), ); log.record(interaction); } // 3. The session is a verifiable ledger. let verified = verify_chain(log.entries()).expect("chain verifies"); println!( "\nsealed interaction chain: {verified} links verified, {} µJ total", log.total_uj() ); println!("every interaction is signed, hash-chained, and energy-accounted — by construction."); }