wire.rsannotatedwire.rssource208 lines · 8.7 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    /// A line after the body, marked with a `!`: what the reader should know about this panel
62    /// (a call that was not sent, and why).
63    pub note: Option<&'a str>,
64}
65
66/// A call as a window that folds (`open` or shut): a title bar (`title`, and `meta` at the right) over its
67/// panels.
68pub fn exchange(icon: &Icon, title: &str, meta: &str, open: bool, panels: &[Panel<'_>]) -> Markup {
69    exchange_with(icon, html! { (title) }, (!meta.is_empty()).then(|| html! { (meta) }), open, html! {}, panels)
70}
71
72/// `exchange` with a title and a `meta` that are markup (a moment the page rewrites as "3 minutes
73/// ago", a name in a link), and `lead`, whatever the site puts first in the body, above the
74/// panels (a choice between earlier answers to the same request).
75pub fn exchange_with(icon: &Icon, title: Markup, meta: Option<Markup>, open: bool, lead: Markup, panels: &[Panel<'_>]) -> Markup {
76    html! {
77        details .win open[open] {
78            summary .bar-title {
79                span .fold {} (pixel_icon(icon, "")) (title)
80                @if let Some(meta) = meta { span .meta { (meta) } }
81            }
82            div .body {
83                (lead)
84                @for panel in panels {
85                    div .wire {
86                        span .label-s { (panel.label) }
87                        @if let Some(head) = panel.head { pre { (head) } }
88                        (json(panel.body))
89                        @if let Some(note) = panel.note { p .note { (note) } }
90                    }
91                }
92            }
93        }
94    }
95}
96
97/// One option of a choice (or the one side of a yes-or-no) and its probability.
98pub struct Bar<'a> {
99    pub label: &'a str,
100    pub p: f64,
101    /// The winner is drawn inverted, with a filled square.
102    pub won: bool,
103}
104
105fn percent(p: f64) -> String {
106    format!("{:.0}%", p * 100.0)
107}
108
109/// Where the square sits on its rule, from a probability.
110fn at(p: f64) -> String {
111    format!("left: {:.1}%", (p * 100.0).clamp(0.0, 100.0))
112}
113
114/// Probabilities as lmjtfy draws them: an inverted label, the figure at the right, and a rule with
115/// a square at the value. `threshold` draws a tick on every rule where a decision is made on it,
116/// named `threshold_label`.
117pub fn bars(bars: &[Bar<'_>], threshold: Option<(f64, &str)>) -> Markup {
118    html! {
119        div .bars {
120            @for bar in bars {
121                div .bar.won[bar.won] {
122                    span .label { (bar.label) }
123                    span .pct { (percent(bar.p)) }
124                    span .track {
125                        @if let Some((t, name)) = threshold { span .cut style=(at(t)) title=(name) {} }
126                        span .thumb style=(at(bar.p)) {}
127                    }
128                }
129            }
130        }
131    }
132}
133
134/// The JSON a panel printed, read back out of its markup: the text with tags gone and entities
135/// undone. A site's test compares it to the body it passed in.
136pub fn printed_json(markup: &str) -> Option<Value> {
137    let mut text = String::new();
138    let mut in_tag = false;
139    for ch in markup.chars() {
140        match ch {
141            '<' => in_tag = true,
142            '>' => in_tag = false,
143            c if !in_tag => text.push(c),
144            _ => {}
145        }
146    }
147    let text = text.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&amp;", "&");
148    serde_json::from_str(&text).ok()
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::components::icons;
155
156    const RAW: &str = r#"{"model":"m","questions":{"b":{"type":"noul","instructions":"Is <it> \"ok\" & fine?"}},"state":{"z":1,"a":[true,null,"x"],"e":{},"l":[]}}"#;
157
158    #[test]
159    fn what_is_printed_is_the_body_with_whitespace_added_and_nothing_else() {
160        let html = json(RAW).into_string();
161        let back = printed_json(&html).expect("the printed text is JSON");
162        let sent: Value = serde_json::from_str(RAW).unwrap();
163        assert_eq!(back, sent);
164        // Keys are in the order they were sent, not sorted.
165        assert_eq!(serde_json::to_string(&back).unwrap(), RAW);
166        assert!(html.contains("<span class=\"k\">"));
167    }
168
169    #[test]
170    fn text_that_is_not_json_is_shown_as_it_is() {
171        assert_eq!(json("upstream <down>").into_string(), "<pre>upstream &lt;down&gt;</pre>");
172    }
173
174    #[test]
175    fn a_call_is_a_window_with_a_labelled_panel_for_each_body() {
176        let html = exchange(
177            &icons::JEV,
178            "jev · 2 questions",
179            "200",
180            true,
181            &[Panel { label: "request", head: Some("POST /v1/systemone"), body: RAW, note: None }, Panel { label: "response", head: None, body: "{\"a\":1}", note: Some("kept") }],
182        )
183        .into_string();
184        assert!(html.starts_with("<details class=\"win\" open>"));
185        assert!(html.contains("<span class=\"label-s\">request</span>") && html.contains("<span class=\"label-s\">response</span>"));
186        assert!(html.contains("<pre>POST /v1/systemone</pre>"));
187        assert!(html.find("request").unwrap() < html.find("response").unwrap());
188        assert!(html.contains("<p class=\"note\">kept</p>") && html.matches("class=\"note\"").count() == 1);
189    }
190
191    #[test]
192    fn a_call_may_lead_with_markup_and_carry_markup_in_its_title_bar() {
193        let html = exchange_with(&icons::TOOL, html! { "tool call · " b { "x" } }, Some(html! { i { "now" } }), false, html! { div #lead {} }, &[]).into_string();
194        assert!(html.starts_with("<details class=\"win\"><summary class=\"bar-title\">"), "{html}");
195        assert!(html.contains("tool call · <b>x</b>") && html.contains("<span class=\"meta\"><i>now</i></span>"));
196        assert!(html.find("id=\"lead\"").unwrap() > html.find("class=\"body\"").unwrap());
197    }
198
199    #[test]
200    fn a_probability_is_a_square_on_a_rule_and_a_threshold_is_a_tick_on_it() {
201        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();
202        assert!(html.contains("<span class=\"pct\">90%</span>") && html.contains("left: 90.0%"));
203        assert_eq!(html.matches("class=\"cut\"").count(), 2);
204        assert!(html.contains("left: 85.0%") && html.contains("title=\"allowed from here\""));
205        assert!(html.contains("class=\"bar won\""));
206        assert!(!bars(&[Bar { label: "x", p: 2.0, won: false }], None).into_string().contains("left: 200"));
207    }
208}