wire.rsannotatedwire.rssource187 lines · 7.2 KB · raw
1//! A call to Jev as a window a visitor can read: the request and the response, printed from the
2//! raw bodies, indented and coloured, and a probability drawn as lmjtfy draws it. This is lmjtfy's
3//! tool-call panel, lifted out so another site shows its own calls the same way.
4//!
5//! The rule it keeps (lmjtfy's, 2026-10-02): **printing may add whitespace and nothing else.** No
6//! reordered keys, no dropped or summarised fields. A site's tests parse what is printed back and
7//! compare it to the body it holds (`printed_json` here does the parsing).
8
9use maud::{Markup, html};
10use serde_json::Value;
11
12use crate::components::{Icon, pixel_icon};
13
14/// A JSON body, indented and coloured for reading. Only whitespace is added: keys keep their order
15/// and every value is printed as it was sent. Text that is not JSON is shown as it is.
16pub fn json(text: &str) -> Markup {
17    match serde_json::from_str::<Value>(text) {
18        Ok(value) => html! { pre .json { (node(&value, 0)) } },
19        Err(_) => html! { pre { (text) } },
20    }
21}
22
23fn node(value: &Value, depth: usize) -> Markup {
24    let indent = "  ".repeat(depth + 1);
25    let close = "  ".repeat(depth);
26    match value {
27        Value::Object(map) if !map.is_empty() => html! {
28            "{\n"
29            @for (i, (key, child)) in map.iter().enumerate() {
30                (indent) span .k { (quoted(key)) } ": " (node(child, depth + 1))
31                @if i + 1 < map.len() { "," } "\n"
32            }
33            (close) "}"
34        },
35        Value::Array(items) if !items.is_empty() => html! {
36            "[\n"
37            @for (i, child) in items.iter().enumerate() {
38                (indent) (node(child, depth + 1))
39                @if i + 1 < items.len() { "," } "\n"
40            }
41            (close) "]"
42        },
43        Value::String(text) => html! { span .s { (quoted(text)) } },
44        // Numbers, booleans, null, and the empty object and array.
45        other => html! { span .n { (other.to_string()) } },
46    }
47}
48
49/// A string as JSON writes it, quotes and escapes included.
50fn quoted(text: &str) -> String {
51    Value::String(text.to_owned()).to_string()
52}
53
54/// One labelled block of a call: an optional plain head (the request line and headers) over a body.
55pub struct Panel<'a> {
56    /// `request`, `response`.
57    pub label: &'a str,
58    /// Lines shown as they are above the body, as `POST /v1/systemone`.
59    pub head: Option<&'a str>,
60    pub body: &'a str,
61}
62
63/// A call as a window that folds (`open` or shut): a title bar (`title`, and `meta` at the right) over its
64/// panels.
65pub fn exchange(icon: &Icon, title: &str, meta: &str, open: bool, panels: &[Panel<'_>]) -> Markup {
66    html! {
67        details .win open[open] {
68            summary .bar-title {
69                span .fold {} (pixel_icon(icon, "")) (title)
70                @if !meta.is_empty() { span .meta { (meta) } }
71            }
72            div .body {
73                @for panel in panels {
74                    div .wire {
75                        span .label-s { (panel.label) }
76                        @if let Some(head) = panel.head { pre { (head) } }
77                        (json(panel.body))
78                    }
79                }
80            }
81        }
82    }
83}
84
85/// One option of a choice (or the one side of a yes-or-no) and its probability.
86pub struct Bar<'a> {
87    pub label: &'a str,
88    pub p: f64,
89    /// The winner is drawn inverted, with a filled square.
90    pub won: bool,
91}
92
93fn percent(p: f64) -> String {
94    format!("{:.0}%", p * 100.0)
95}
96
97/// Where the square sits on its rule, from a probability.
98fn at(p: f64) -> String {
99    format!("left: {:.1}%", (p * 100.0).clamp(0.0, 100.0))
100}
101
102/// Probabilities as lmjtfy draws them: an inverted label, the figure at the right, and a rule with
103/// a square at the value. `threshold` draws a tick on every rule where a decision is made on it,
104/// named `threshold_label`.
105pub fn bars(bars: &[Bar<'_>], threshold: Option<(f64, &str)>) -> Markup {
106    html! {
107        div .bars {
108            @for bar in bars {
109                div .bar.won[bar.won] {
110                    span .label { (bar.label) }
111                    span .pct { (percent(bar.p)) }
112                    span .track {
113                        @if let Some((t, name)) = threshold { span .cut style=(at(t)) title=(name) {} }
114                        span .thumb style=(at(bar.p)) {}
115                    }
116                }
117            }
118        }
119    }
120}
121
122/// The JSON a panel printed, read back out of its markup: the text with tags gone and entities
123/// undone. A site's test compares it to the body it passed in.
124pub fn printed_json(markup: &str) -> Option<Value> {
125    let mut text = String::new();
126    let mut in_tag = false;
127    for ch in markup.chars() {
128        match ch {
129            '<' => in_tag = true,
130            '>' => in_tag = false,
131            c if !in_tag => text.push(c),
132            _ => {}
133        }
134    }
135    let text = text.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&amp;", "&");
136    serde_json::from_str(&text).ok()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::components::icons;
143
144    const RAW: &str = r#"{"model":"m","questions":{"b":{"type":"noul","instructions":"Is <it> \"ok\" & fine?"}},"state":{"z":1,"a":[true,null,"x"],"e":{},"l":[]}}"#;
145
146    #[test]
147    fn what_is_printed_is_the_body_with_whitespace_added_and_nothing_else() {
148        let html = json(RAW).into_string();
149        let back = printed_json(&html).expect("the printed text is JSON");
150        let sent: Value = serde_json::from_str(RAW).unwrap();
151        assert_eq!(back, sent);
152        // Keys are in the order they were sent, not sorted.
153        assert_eq!(serde_json::to_string(&back).unwrap(), RAW);
154        assert!(html.contains("<span class=\"k\">"));
155    }
156
157    #[test]
158    fn text_that_is_not_json_is_shown_as_it_is() {
159        assert_eq!(json("upstream <down>").into_string(), "<pre>upstream &lt;down&gt;</pre>");
160    }
161
162    #[test]
163    fn a_call_is_a_window_with_a_labelled_panel_for_each_body() {
164        let html = exchange(
165            &icons::JEV,
166            "jev · 2 questions",
167            "200",
168            true,
169            &[Panel { label: "request", head: Some("POST /v1/systemone"), body: RAW }, Panel { label: "response", head: None, body: "{\"a\":1}" }],
170        )
171        .into_string();
172        assert!(html.starts_with("<details class=\"win\" open>"));
173        assert!(html.contains("<span class=\"label-s\">request</span>") && html.contains("<span class=\"label-s\">response</span>"));
174        assert!(html.contains("<pre>POST /v1/systemone</pre>"));
175        assert!(html.find("request").unwrap() < html.find("response").unwrap());
176    }
177
178    #[test]
179    fn a_probability_is_a_square_on_a_rule_and_a_threshold_is_a_tick_on_it() {
180        let html = bars(&[Bar { label: "yes", p: 0.9, won: true }, Bar { label: "no", p: 0.1, won: false }], Some((0.85, "allowed from here"))).into_string();
181        assert!(html.contains("<span class=\"pct\">90%</span>") && html.contains("left: 90.0%"));
182        assert_eq!(html.matches("class=\"cut\"").count(), 2);
183        assert!(html.contains("left: 85.0%") && html.contains("title=\"allowed from here\""));
184        assert!(html.contains("class=\"bar won\""));
185        assert!(!bars(&[Bar { label: "x", p: 2.0, won: false }], None).into_string().contains("left: 200"));
186    }
187}