lmjtfy.git / apps / lmjtfy / src / view.rs
view.rsannotatedview.rssource1451 lines · 58.7 KB · raw

The page and the transcript, as HTML. Pure: a View in, markup out.

3use crate::diagram;
4use crate::host::Host;
5use crate::meatproxy;
7pub mod code;
8pub mod releases;
9pub mod repos;
10pub mod toast;
11
12use std::time::Duration;
13
14use archive::{Answer, Detail, Entry, Home, Stats, when};
15use ask::{Judged, Outcome, Prepared, Sent};
16use jev_protocol::{ChoiceAnswer, ScoreAnswer};
17use llm::{Draft, ToolCall};
18use maud::{DOCTYPE, Markup, PreEscaped, html};
19use rules::{Fact, Kind, Known, Network};
20use serde_json::Value;
21
22const CSS: &str = include_str!("page.css");
23const JS: &str = include_str!("page.js");

The nearest open faces to typesafe.ai's own: Inter Tight for its grotesk, VT323 for its pixel terminal face, and JetBrains Mono, which it also uses.

26const FONTS: &str = "https://fonts.googleapis.com/css2?family=Inter+Tight:wght@500&family=JetBrains+Mono:wght@400;500&family=VT323&display=swap";

Pixel icons for the window title bars, 12 by 12, # for ink.

29mod icons {
30    pub const ASK: [&str; 12] = [
31        "............", "..#####.....", ".#.....#....", ".#.....#....", ".#.....#....", ".#.....#....",
32        "..#####.....", ".......##...", "........##..", ".........##.", "..........#.", "............",
33    ];
34    pub const JEV: [&str; 12] = [
35        ".....##.....", "....#.##....", "...#.#.##...", "..#.#.#.##..", ".#.#.#.#.##.", "#.#.#.#.####",
36        "####.#.#.#.#", ".##.#.#.#.#.", "..##.#.#.#..", "...##.#.#...", "....##.#....", ".....##.....",
37    ];
38    pub const LLM: [&str; 12] = [
39        "............", ".#########..", ".#.......#..", ".#.##.##.#..", ".#.......#..", ".#.#####.#..",
40        ".#.......#..", ".#########..", "...##.......", "..##........", ".#..........", "............",
41    ];
42    pub const TOOL: [&str; 12] = [
43        "............", ".......###..", "......##....", "......##..#.", "......#####.", ".....####...",
44        "....###.....", "...###......", "..###.......", ".###........", ".##.........", "............",
45    ];
46}

A pixel icon as inline SVG: one square per #.

49fn icon(rows: &[&str; 12], class: &str) -> Markup {
50    html! {
51        svg class={ "icon " (class) } viewBox="0 0 12 12" aria-hidden="true" {
52            @for (y, row) in rows.iter().enumerate() {
53                @for (x, cell) in row.bytes().enumerate() {
54                    @if cell == b'#' { rect x=(x) y=(y) width="1" height="1" fill="currentColor" {} }
55                }
56            }
57        }
58    }
59}

One request to Jev, before and after its response. It may ask several questions.

63pub struct JevCall {

What the request is for: facts or answers.

65    pub id: &'static str,

Each question's id and Jev type, in the order asked.

67    pub parts: Vec<(String, &'static str)>,

The request body as sent.

69    pub request: String,

None while Jev is still answering.

71    pub outcome: Option<Outcome>,

Which of the request's kept responses this is, once one is.

73    pub kept: Option<Versions>,
74}

A call's place among the responses its request got: version version of versions, and its name for the page's ↻ and ◀ ▶ (archive::call_id).

78#[derive(Clone, Debug, PartialEq)]
79pub struct Versions {
80    pub id: String,
81    pub version: u32,
82    pub versions: u32,
83}
85impl JevCall {
86    pub fn pending(prepared: &Prepared) -> Self {
87        JevCall {
88            id: prepared.id,
89            parts: prepared.parts.iter().map(|part| (part.id.clone(), part.kind)).collect(),
90            request: prepared.request.clone(),
91            outcome: None,
92            kept: None,
93        }
94    }

The answer to the question called id, if Jev gave one.

97    pub fn judged(&self, id: &str) -> Option<&Judged> {
98        let index = self.parts.iter().position(|(part, _)| part == id)?;
99        match &self.outcome {
100            Some(Outcome::Answered { judged, .. }) => judged.get(index),
101            _ => None,
102        }
103    }
104}

The call to the LLM that writes Jev's questions.

107pub struct LlmCall {
108    pub model: &'static str,

The request body as sent.

110    pub request: String,

None while the LLM is still writing.

112    pub outcome: Option<LlmOutcome>,

Which of the request's kept responses this is, once one is.

114    pub kept: Option<Versions>,
115}
117pub struct LlmOutcome {

The reply as it came, or the error in its place.

119    pub body: String,

What the reply cost when it was sent.

121    pub neurons: f64,
122    pub took: Duration,

Tool calls the LLM made past the cap, which were not run.

124    pub dropped: usize,

Whether this ask sent the request, or was given the reply kept from the first time.

127    pub sent: Sent,
128}

One tool call the LLM made.

131pub struct Tool {

Its question's id in the request to Jev: q1, q2, ...

133    pub id: String,
134    pub call: ToolCall,

Why Jev's own protocol refused a call whose shape was right.

136    pub refused: Option<String>,
137}
139impl Tool {

The question to send to Jev, if the call is one.

141    pub fn draft(&self) -> Option<&Draft> {
142        self.call.draft.as_ref().ok().filter(|_| self.refused.is_none())
143    }

Why this call was not sent to Jev, if it was not.

146    fn problem(&self) -> Option<&str> {
147        match &self.call.draft {
148            Err(error) => Some(error),
149            Ok(_) => self.refused.as_deref(),
150        }
151    }
152}

How the exchange ended.

155pub enum Ending {

Nothing was typed; nothing was sent.

157    Empty,

The gate said this is not something Jev can judge.

159    NotAQuestion { p_yes: f64 },

Jev answered: the input itself, read one way or two, or what the LLM wrote. Which, is in the rules' notes.

162    Answered,

This visitor has asked too much in the last minute. Nothing was sent.

164    Slow,

The LLM replied, and nothing in the reply was a question Jev answered.

166    NoQuestion,

A call failed, or could not be built.

168    Failed { error: String },

The Worker has no API key.

170    Offline,

Today's shared budget for what is used up.

172    Spent { what: &'static str },

The page is looking at an old version of a call, and the call after it was never sent: nothing is sent while looking, so it waits for ↻.

175    NotKept { id: String },
176}

One answer in the transcript.

179struct Shown<'a> {

The question it answers: the input, or what the LLM wrote.

181    question: &'a str,
182    judged: &'a Judged,

How the input was read, when it was read more than one way.

184    read_as: Option<&'static str>,

A how-likely: the answer is the probability itself, not a yes or no.

186    chance: bool,
187}
189impl Shown<'_> {

The answer as it is kept for the feed and for link previews.

191    fn told(&self) -> Answer {
192        let detail = match self.judged {
193            Judged::Noul(p) if self.chance => Detail::Chance { p: *p },
194            Judged::Noul(p_yes) => Detail::Noul { p_yes: *p_yes },
195            Judged::Choice(answer) => {
196                let mut options = answer.probabilities.clone();
197                options.sort_by(|a, b| b.1.total_cmp(&a.1));
198                Detail::Choice { confidence: answer.confidence, options }
199            }
200            Judged::Score(answer) => Detail::Score {
201                score: answer.score,
202                confidence: answer.confidence,
203                levels: answer.legend.iter().cloned().zip(answer.probabilities.iter().copied()).collect(),
204            },
205        };
206        Answer { headline: self.headline(), detail }
207    }

The answer in a few words: what the transcript prints large.

210    fn headline(&self) -> String {
211        match self.judged {
212            Judged::Noul(p) if self.chance => format!("{}.", percent(*p)),
213            judged => headline(judged),
214        }
215    }
216}

Everything the transcript shows.

219pub struct View {
220    pub input: String,

What is known about the input so far: what the rules run on.

222    pub known: Known,

The rules that decided a step, in order (indices into the network's terminals).

225    pub fired: Vec<usize>,

The request for the facts the rules wanted.

227    pub facts: Option<JevCall>,
228    pub llm: Option<LlmCall>,
229    pub tools: Vec<Tool>,

The request that asks Jev every question the LLM wrote.

231    pub judging: Option<JevCall>,

None while calls are still being made.

233    pub ending: Option<Ending>,

The card of the LLM suggestion's site, read before the ending is shown (meatproxy::head); nothing if it could not be.

236    pub preview: Option<meatproxy::Head>,

Which version of each call the page asked for ($pins).

238    pub pins: archive::Pins,
239}
241impl View {
242    pub fn new(input: String) -> Self {
243        View {
244            input,
245            known: Known::default(),
246            fired: Vec::new(),
247            facts: None,
248            llm: None,
249            tools: Vec::new(),
250            judging: None,
251            ending: None,
252            preview: None,
253            pins: archive::Pins::default(),
254        }
255    }

$pins once this view is shown: every call shown at a version other than its newest, so the page's next ↻ or ◀ ▶ keeps them as they are.

259    pub fn pinned(&self) -> String {
260        let kept = [self.facts.as_ref().and_then(|call| call.kept.as_ref()), self.llm.as_ref().and_then(|call| call.kept.as_ref()), self.judging.as_ref().and_then(|call| call.kept.as_ref())];
261        let pins: Vec<String> = kept.into_iter().flatten().filter(|kept| kept.version < kept.versions).map(|kept| format!("{}:{}", kept.id, kept.version)).collect();
262        pins.join(";")
263    }

How many of this view's calls were sent for it, and how many were answered from what was kept.

267    pub fn calls(&self) -> (u32, u32) {
268        let jev = [&self.facts, &self.judging].into_iter().flatten().filter_map(|call| match &call.outcome {
269            Some(Outcome::Answered { sent, .. }) => Some(*sent),
270            _ => None,
271        });
272        let llm = self.llm.as_ref().and_then(|call| call.outcome.as_ref()).map(|outcome| outcome.sent);
273        let (mut sent, mut kept) = (0, 0);
274        for call in jev.chain(llm) {
275            match call {
276                Sent::Now => sent += 1,
277                Sent::Before { .. } => kept += 1,
278            }
279        }
280        (sent, kept)
281    }
283    fn fact(&self, fact: Fact) -> Option<&Judged> {
284        self.facts.as_ref()?.judged(ask::question_id(fact))
285    }
286
287    fn judged(&self, tool: &Tool) -> Option<&Judged> {
288        self.judging.as_ref()?.judged(&tool.id)
289    }

The kind Jev thinks the input most likely is.

292    pub fn reading(&self) -> Option<Kind> {
293        match self.fact(Fact::Reads(Kind::Noul))? {
294            Judged::Choice(answer) => ask::readings(answer).first().copied(),
295            _ => None,
296        }
297    }

What to print for a fact on the diagram: Jev's own number for a fact it answered, a count for the others.

301    fn shown(&self, fact: Fact) -> Option<String> {
302        match fact {
303            Fact::Drafted => Some(self.tools.iter().filter(|tool| tool.draft().is_some()).count().to_string()),
304            Fact::Judged => Some(self.tools.iter().filter(|tool| self.judged(tool).is_some()).count().to_string()),
305            Fact::Reads(kind) => match self.fact(fact)? {
306                Judged::Choice(answer) => ask::kind_probability(answer, kind).map(probability),
307                _ => None,
308            },
309            _ => match self.fact(fact)? {
310                Judged::Noul(p_yes) => Some(probability(*p_yes)),
311                Judged::Score(answer) => Some(format!("{:.1}", answer.score)),
312                Judged::Choice(_) => None,
313            },
314        }
315    }

Whether Jev answered any question the LLM wrote.

318    pub fn any_judged(&self) -> bool {
319        self.tools.iter().any(|tool| self.judged(tool).is_some())
320    }

The answers the rules say to show, in rule order: the input itself as Jev answered it with the facts (read as a yes-or-no, as a how-much, or both when Jev was split), then what the LLM wrote.

325    fn results(&self) -> Vec<Shown<'_>> {
326        let mut results = Vec::new();
327        for fact in Network::lmjtfy().shows(&self.known) {
328            if fact == Fact::Judged {
329                results.extend(self.tools.iter().filter_map(|tool| {
330                    let question = tool.call.draft.as_ref().ok()?.instructions();
331                    Some(Shown { question, judged: self.judged(tool)?, read_as: None, chance: false })
332                }));
333            } else if let Some(judged) = self.fact(fact) {
334                let read_as = match fact {
335                    Fact::Yes => "read as a yes or no",
336                    Fact::Chance => "read as a how likely",
337                    _ => "read as a how much",
338                };
339                results.push(Shown { question: &self.input, judged, read_as: Some(read_as), chance: fact == Fact::Chance });
340            }
341        }
342        // The reading is worth saying only when there is another beside it.
343        if results.len() == 1 {
344            results[0].read_as = None;
345        }
346        results
347    }

Whether there is any answer to show.

350    pub fn answered(&self) -> bool {
351        !self.results().is_empty()
352    }

What Jev said to each question it answered, as it is kept for the feed and for link previews.

356    pub fn said(&self) -> Vec<Answer> {
357        self.results().iter().map(Shown::told).collect()
358    }
359}

What is used of the budgets every visitor shares, and their limits.

362pub struct Shared {
363    pub status: budget::Status,
364    pub neurons_per_day: f64,
365    pub jev_dollars_per_day: f64,
366}

A whole number with thousands separators.

369fn grouped(n: f64) -> String {
370    let digits = format!("{:.0}", n.max(0.0));
371    let mut out = String::new();
372    for (i, digit) in digits.chars().enumerate() {
373        if i > 0 && (digits.len() - i) % 3 == 0 {
374            out.push(',');
375        }
376        out.push(digit);
377    }
378    out
379}

The shared budgets, as a window everyone sees: what is left today, whose count it is, and that it is one pot. With no figures to show, it says so.

383pub fn budget(shared: Option<Shared>) -> Markup {
384    html! {
385        div #budget .win {
386            div .bar-title { (icon(&icons::JEV, "")) "Shared budget" span .meta { "resets 00:00 UTC" } }
387            div .body {
388                @if let Some(shared) = shared {
389                    @let left = (shared.neurons_per_day - shared.status.neurons).max(0.0);
390                    @let jev_left = (shared.jev_dollars_per_day - shared.status.jev_dollars).max(0.0);
391                    div .bars {
392                        div .bar.won {
393                            span .label { "LLM · free neurons left today" }
394                            span .pct { (grouped(left)) " of " (grouped(shared.neurons_per_day)) }
395                            span .track { span .thumb style=(at(left / shared.neurons_per_day)) {} }
396                        }
397                        div .bar.won {
398                            span .label { "Jev · dollars left today" }
399                            span .pct { (format!("${jev_left:.4} of ${:.2}", shared.jev_dollars_per_day)) }
400                            span .track { span .thumb style=(at(jev_left / shared.jev_dollars_per_day)) {} }
401                        }
402                    }
403                    p .fine {
404                        "Everyone who opens this page spends from the same two pots. When one is empty, "
405                        "the site stops answering until it resets. "
406                        @match shared.status.counted {
407                            budget::Counted::Account => "The neuron figure is Cloudflare's own count for the whole account.",
408                            budget::Counted::Own => "The neuron figure is this site's own count; Cloudflare's could not be read.",
409                        }
410                    }
411                } @else {
412                    p .fine { "The shared budget could not be read just now." }
413                }
414            }
415        }
416    }
417}

A line of a feed. A line of "asked lately" has an id from its question, so the line a question already has is the one a live update moves to the top; "most asked" repeats some of those questions, and has none.

422fn entry(entry: &Entry, id: Option<String>) -> Markup {
423    html! {
424        li .entry id=[id] {
425            a href={ "/?q=" (url_encode(&entry.input)) } title=(when(entry.asked_ms)) { (entry.input) }
426            span .said-s {
427                (entry.headlines())
428                @if entry.times > 1 { span .times { " ×" (entry.times) } }
429            }
430        }
431    }
432}
434fn lately_entry(asked: &Entry) -> Markup {
435    entry(asked, Some(entry_id(&asked.input)))
436}

The id of a question's line in the feed: FNV-1a of the question, so it is the same on every page and for every render.

440fn entry_id(input: &str) -> String {
441    let hash = input.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3));
442    format!("q-{hash:016x}")
443}

The end of "asked lately". A full page may have more after it, so when it scrolls into view it asks /feed for the page after the last line shown, once: the answer replaces it, with a new end that asks again. A short page is the end of the feed, and asks nothing.

449fn more(shown: &[Entry]) -> Markup {
450    let after = (shown.len() == archive::FEED as usize).then(|| shown.last()).flatten();
451    html! {
452        @if let Some(last) = after {
453            li #more "data-on-intersect__once"={
454                "@get('/feed?ms=" (last.asked_ms) "&q=" (url_encode(&last.input)) "')"
455            } { span .meta { "more…" } }
456        } @else {
457            li #more hidden {}
458        }
459    }
460}

The page of the feed after a scroll: its lines, which go before the end, and the new end.

464pub fn older(entries: &[Entry]) -> (Markup, Markup) {
465    (html! { @for asked in entries { (lately_entry(asked)) } }, more(entries))
466}

A question just asked, for the top of every open page's feed.

469pub fn lately_top(entry: &Entry) -> Markup {
470    html! { ol hidden "data-prepend"="lately" { (lately_entry(entry)) } }
471}

The most asked questions, replaced whole when they change.

474pub fn most(most: &[Entry]) -> Markup {
475    html! {
476        div #most {
477            @if !most.is_empty() {
478                span .label-s { "most asked" }
479                ol .feed { @for asked in most { (entry(asked, None)) } }
480            }
481        }
482    }
483}

What people asked and what Jev said: lately, and most often. Each is a link that asks it again, which sends nothing: every call it needs is kept. Nothing about who asked is shown, and a question Jev judged unfit to show is not listed. "Asked lately" scrolls on for as long as there is feed (more). The window is there even when the feed is empty, and hidden by the style sheet until a line arrives, so a live update has somewhere to put the first one.

491pub fn feed(home: Option<&Home>) -> Markup {
492    let lately = home.map(|home| home.lately.as_slice()).unwrap_or_default();
493    let most_asked = home.map(|home| home.most.as_slice()).unwrap_or_default();
494    html! {
495        div #feed .win {
496            div .bar-title { (icon(&icons::ASK, "")) "Asked lately" span .meta { "by nobody in particular" } }
497            div .body {
498                ol #lately .feed { @for asked in lately { (lately_entry(asked)) } (more(lately)) }
499                (most(most_asked))
500            }
501        }
502    }
503}
505fn plural(n: u32, one: &str, many: &str) -> String {
506    format!("{} {}", grouped(f64::from(n)), if n == 1 { one } else { many })
507}

The archive in numbers: how much Jev settled alone, and how many requests were never sent because their response was kept.

511pub fn tally(stats: Option<&Stats>) -> Markup {
512    let stats = stats.filter(|stats| stats.questions > 0);
513    html! {
514        div #tally .win[stats.is_some()] {
515            @if let Some(stats) = stats {
516                div .bar-title { (icon(&icons::JEV, "")) "So far" }
517                div .body {
518                    ul .tally {
519                        li {
520                            b { (plural(stats.questions, "question", "questions")) }
521                            " answered, asked " (plural(stats.asks, "time", "times")) "."
522                        }
523                        @if let Some(percent) = stats.no_llm_percent() {
524                            li { b { (percent) "%" } " of them Jev answered alone, with no LLM." }
525                        }
526                        li {
527                            b { (plural(stats.sent, "request", "requests")) }
528                            " sent to Jev and the LLM. "
529                            b { (grouped(f64::from(stats.kept))) }
530                            " more were not sent, because the same request had been answered before."
531                        }
532                        @if stats.clones + stats.pulls > 0 {
533                            li {
534                                "The code was " a href=(CODE) { "cloned" } " "
535                                b { (plural(stats.clones, "time", "times")) } " and pulled "
536                                b { (plural(stats.pulls, "time", "times")) } "."
537                            }
538                        }
539                    }
540                }
541            }
542        }
543    }
544}

A short mark of an answer, for the address of its picture: whoever shows previews keeps a picture by its address, so the address has to change when the answer does (a question asked for the first time, most of all).

549fn mark(text: &str) -> String {
550    // FNV-1a.
551    let hash = text.bytes().fold(0x811c_9dc5_u32, |hash, byte| (hash ^ u32::from(byte)).wrapping_mul(0x0100_0193));
552    format!("{hash:08x}")
553}

What a link to the page unfurls as, in Discord and wherever else reads Open Graph tags. The home page says what the site is. A ?q= link shows the question, and Jev's answer when it has been asked before: a preview is built from what is kept and never asks anything. origin is where the site is, for the picture's address.

560fn preview(typed: &str, said: Option<&Entry>, declined: bool, origin: &str) -> Markup {
561    // Each answer with its numbers: the probability, the confidence, the
562    // likeliest options. That is half of what Jev says.
563    let answer = said.filter(|entry| !entry.answers.is_empty()).map(|entry| {
564        let lines: Vec<String> = entry.answers.iter().map(Answer::line).collect();
565        lines.join("  /  ")
566    });
567    let (title, description, picture) = if typed.is_empty() {
568        (
569            "Let Me Jev That For You".to_owned(),
570            "Ask Jev anything. It answers yes, no, which one or how much, with a probability, and shows its work."
571                .to_owned(),
572            format!("{origin}/card.png"),
573        )
574    } else {
575        // A question Jev could not take says so, and nothing of Jev's: it
576        // points at the LLMs the page does (`meatproxy`), and its picture
577        // is the card for that (`card::Card::Declined`).
578        let declined = declined && answer.is_none();
579        let description = match &answer {
580            Some(answer) => format!("Jev: {answer}"),
581            None if declined => "Please choose an LLM instead: Claude, ChatGPT or Gemini.".to_owned(),
582            None => "Let me Jev that for you.".to_owned(),
583        };
584        let picture = format!(
585            "{origin}/card.png?q={}&a={}",
586            url_encode(typed),
587            mark(if declined { "declined" } else { answer.as_deref().unwrap_or_default() })
588        );
589        (format!("Q: {typed}"), description, picture)
590    };
591    html! {
592        title { (title) }
593        meta name="description" content=(description);
594        meta property="og:type" content="website";
595        meta property="og:site_name" content="LMJTFY · Let Me Jev That For You";
596        meta property="og:title" content=(title);
597        meta property="og:description" content=(description);
598        meta property="og:image" content=(picture);
599        meta property="og:image:type" content="image/png";
600        meta property="og:image:width" content=(card::WIDTH);
601        meta property="og:image:height" content=(card::HEIGHT);
602        // The picture is shown large, under the text.
603        meta name="twitter:card" content="summary_large_image";
604        // The bar down the side of the embed: the page's pink.
605        meta name="theme-color" content="#f386a1";
606    }
607}

The start of every page: what it unfurls as, the type and the look.

610fn head(preview: Markup) -> Markup {
611    html! {
612        head {
613            meta charset="utf-8";
614            meta name="viewport" content="width=device-width, initial-scale=1";
615            // The build this page came from, for `/live` to compare (page.js).
616            meta name="lmjtfy-build" content=(crate::BUILD);
617            (preview)
618            link rel="preconnect" href="https://fonts.googleapis.com";
619            link rel="preconnect" href="https://fonts.gstatic.com" crossorigin;
620            link rel="stylesheet" href=(FONTS);
621            style { (PreEscaped(CSS)) }
622        }
623    }
624}

The bar at the top of every page. On the code host (host) the title leads home to the other address, and there is no online count or toasts: that host has no /live, and page.js, finding no #online, opens no socket and sends no /seen.

630fn nav(host: Host) -> Markup {
631    html! {
632        nav {
633            a href=(host.home_link()) { "LMJTFY" }
634            @if host.is_live() {
635                // Filled in over `/live` (page.js): pages open now, and a toast
636                // for each question asked and each clone while this one is open.
637                span .online-wrap tabindex="0" {
638                    span #online .online hidden {}
639                    (places(&[]))
640                }
641                div #toasts aria-live="polite" {}
642            }
643            a .dark href=(CODE) { "Code" }
644            a .dark href="https://docs.typesafe.ai" { "What is Jev?" }
645        }
646    }
647}

Whether Jev was right, as the people who asked think: a vote each, one per browser, taken back by pressing it again. rating is None when there is nothing to vote on yet; can is false for a browser with no id.

652pub fn rating(rating: Option<&archive::Rating>, can: bool) -> Markup {
653    // The thumbs mean otherwise on a question Jev declined: up is "right to
654    // pass", down is "could have answered".
655    let declined = rating.is_some_and(|rating| rating.declined);
656    let button = |vote: archive::Vote, glyph: &str, n: u32, label: &str| {
657        let on = rating.and_then(|rating| rating.mine) == Some(vote);
658        let name = match vote {
659            archive::Vote::Up => "up",
660            archive::Vote::Down => "down",
661        };
662        html! {
663            button .vote .on[on] type="button" disabled[!can] aria-pressed=(on) title=(label)
664                "data-on:click"={ "$vote = '" (name) "'; @post('/rate')" } {
665                span .glyph aria-hidden="true" { (glyph) } " " (n)
666            }
667        }
668    };
669    html! {
670        div #rating .rating {
671            @if let Some(shown) = rating {
672                @if declined {
673                    span .label-s { "Was Jev right to pass?" }
674                    (button(archive::Vote::Up, "👍", shown.up, "Jev was right to pass"))
675                    (button(archive::Vote::Down, "👎", shown.down, "Jev could have answered"))
676                } @else {
677                    span .label-s { "Was Jev right?" }
678                    (button(archive::Vote::Up, "👍", shown.up, "Jev was right"))
679                    (button(archive::Vote::Down, "👎", shown.down, "Jev was wrong"))
680                }
681                @if !can { span .meta { "Voting needs this site's cookie: reload the page." } }
682            }
683        }
684    }
685}

The box that opens under the vote (/comment). It is on the page from the first answer and opens by the open signal, which /rate sets while the browser has a vote, so the transition is the page's own CSS and a pressed vote costs no markup. It is never patched but for its two notes: the text lives in the comment signal, so a patch cannot take it away, and a failed save leaves it where it was. Closed, it is inert, so it is not a tab stop either.

Under a question Jev declined (declined) it asks why Jev could have answered, and how the site can be better.

697pub fn comment_box(declined: bool) -> Markup {
698    let placeholder = if declined { "Why could Jev have answered? Or how can we improve?" } else { "Let us know what we can improve" };
699    html! {
700        div #comment .comment data-class:open="$open" data-attr:inert="!$open" {
701            div .comment-inner {
702                label .label-s for="comment-text" { "Comments?" }
703                textarea #comment-text .comment-text name="comment" rows="3" maxlength=(archive::MOST_COMMENT)
704                    placeholder=(placeholder) data-bind:comment "data-on:input"="$saved = false" {}
705                div .comment-actions {
706                    button .vote type="button" data-attr:disabled="$saved || $comment.trim() === ''" "data-on:click"="@post('/comment')" { "Save" }
707                    span #comment-thanks .thanks data-class:shown="$saved" role="status" { "Thanks! Your comment was saved." }
708                    (comment_error(None))
709                }
710            }
711        }
712    }
713}

Why a comment was not saved, said where the Save button is. Fixed words: nothing the visitor typed is ever echoed into it.

717pub fn comment_error(unsaid: Option<archive::Unsaid>) -> Markup {
718    use archive::Unsaid;
719    html! {
720        span #comment-error .meta role="alert" {
721            @match unsaid {
722                None => {}
723                Some(Unsaid::Empty) => { "Write something first." }
724                Some(Unsaid::TooLong) => { "That is over " (archive::MOST_COMMENT) " characters: shorten it and Save again." }
725                Some(Unsaid::NoVote) => { "Vote first, then comment." }
726                Some(Unsaid::Limit) => { "Slow down a little: try again in a minute." }
727                Some(Unsaid::Failed) => { "That could not be saved. Your text is still here: try again." }
728            }
729        }
730    }
731}

Where the open pages are, for the drop-down under "N online": a flag and a place a line, most first. Replaced whole over /live when a page comes or goes.

736pub fn places(open: &[(archive::Place, u32)]) -> Markup {
737    html! {
738        ul #places .places {
739            @for (place, n) in open {
740                li {
741                    span .flag aria-hidden="true" { (place.flag().unwrap_or_else(|| "·".to_owned())) }
742                    span .where { (place.label()) }
743                    @if *n > 1 { span .times { "×" (n) } }
744                }
745            }
746        }
747    }
748}

Where the code is cloned from, and its landing page: the same address.

751const CODE: &str = "/lmjtfy.git";

The foot of the home page and the code page: what the site is, and on the home page, how to get its code (the code page has it at the top).

755fn footer(clone: Option<&str>) -> Markup {
756    html! {
757        footer {
758            div .wrap {
759                div .about {
760                    (icon(&icons::JEV, "big"))
761                    span {
762                        "Jev answers typed questions with calibrated probabilities, not text. "
763                        "Rules decide what to ask it, and an LLM writes the options when a question needs them. "
764                        a href="/rules" { "See the rules." } " "
765                        "Not affiliated with TypeSafe AI."
766                    }
767                }
768                @if let Some(origin) = clone {
769                    (clone_line(&crate::clone::command(origin), html! { a href={ (origin) (CODE) } { "Get the code:" } }))
770                }
771            }
772        }
773    }
774}

A clone command with a copy button.

777fn clone_line(command: &str, label: Markup) -> Markup {
778    clone_line_as("clone", command, label)
779}

The same, for a page with several: id is what the copy button copies (page.js's data-copy), so it is one per command on the page.

783fn clone_line_as(id: &str, command: &str, label: Markup) -> Markup {
784    html! {
785        div .share.clone {
786            span { (label) }
787            code id=(id) { (command) }
788            button .ghost type="button" data-copy=(id) { "Copy" }
789        }
790    }
791}

The rules with facts set by hand (playground.rs): every test is a link.

794pub fn playground(
795    network: &Network,
796    known: &Known,
797    fired: &[usize],
798    says: &str,
799    link: impl Fn(diagram::Clicked) -> String,
800) -> Markup {
801    let title = "The rules · LMJTFY";
802    let about = "The rules that decide what happens to a question, as a Rete network you can set the facts of.";
803    html! {
804        (DOCTYPE)
805        html lang="en" {
806            (head(html! {
807                title { (title) }
808                meta name="description" content=(about);
809                meta property="og:title" content=(title);
810                meta property="og:description" content=(about);
811                meta name="theme-color" content="#f386a1";
812            }))
813            body {
814                div .sheet {
815                    div .wrap {
816                        (nav(Host::Home))
817                        div .stack {
818                            div .win {
819                                div .bar-title {
820                                    (icon(&icons::TOOL, "")) "rules · rete"
821                                    span .meta {
822                                        @if fired.is_empty() { "nothing decided yet" } @else { (diagram::sequence(network, fired)) }
823                                    }
824                                }
825                                div .body {
826                                    (diagram::rete(network, known, fired, |_| None, |clicked| Some(link(clicked))))
827                                    p .says { (says) }
828                                    p .fine {
829                                        "Click a test to say its fact is so, or a fact to step through what it can be. "
830                                        "Nothing is asked of Jev or the LLM here: the facts are whatever you set. "
831                                        a href="/rules" { "Start over." }
832                                    }
833                                    p .fine {
834                                        "On a real question, Jev gives every fact in the first frame in one "
835                                        "request, and the rules take it from there. Filled is what a rule that "
836                                        "holds stands on, outlined is known and not needed, dashed failed."
837                                    }
838                                }
839                            }
840                        }
841                    }
842                }
843                // The online count and the toasts, as on every page.
844                script { (PreEscaped(JS)) }
845            }
846        }
847    }
848}

said is the kept answer to typed, if there is one, for the preview.

origin is where this page is, for the preview's picture; code is where the code is cloned from (host::Host::code_origin), for the command at the foot.

854pub fn page(typed: &str, said: Option<&Entry>, declined: bool, origin: &str, code: &str, shared: Option<Shared>, home: Option<&Home>) -> Markup {
855    html! {
856        (DOCTYPE)
857        html lang="en" {
858            (head(html! {
859                (preview(typed, said, declined, origin))
860                script type="module" src="/datastar.js" {}
861            }))
862            body data-signals="{q: '', pins: '', comment: '', open: false, saved: false}" {
863                div .sheet {
864                    div .wrap {
865                        (nav(Host::Home))
866                        div .headline {
867                            p .eyebrow { "Let me ........................ Jev that for you" }
868                            h1 { "Let Me Jev That For You" }
869                        }
870                        div .stack {
871                            div .win {
872                                div .bar-title { (icon(&icons::ASK, "")) "Ask" }
873                                div .body {
874                                    form #ask data-type=(typed) data-on:submit__prevent="@post('/ask')" data-indicator:asking {
875                                        // Jev answers in about a fifth of a second, so the
876                                        // gate runs on every pause in the typing.
877                                        // A textarea so a long question wraps and the box grows
878                                        // with it (page.css, page.js); Enter still asks, and a
879                                        // question is one line, so no newline gets in (page.js).
880                                        textarea #q name="q" rows="1" data-bind:q "data-on:input__debounce.300ms"="@post('/gate')"
881                                            autocomplete="off" required maxlength=(ask::MAX_INPUT_CHARS)
882                                            placeholder="Ask Jev anything" aria-label="Your question" {}
883                                        button type="submit" data-attr:disabled="$asking" { "Ask Jev" }
884                                    }
885                                    (suggested(&[]))
886                                    (live(None))
887                                }
888                            }
889                            (budget(shared))
890                            (feed(home))
891                            (tally(home.map(|home| &home.stats)))
892                        }
893                    }
894                }
895                (resting())
896                (footer(Some(code)))
897                script { (PreEscaped(JS)) }
898            }
899        }
900    }
901}

What the facts say about the text so far, for the line under the box.

904pub struct Glance {

Jev's probability that it can judge the text.

906    pub answerable: f64,
907    pub kind: Option<Kind>,

The rules' note that the text stops part way (Note::Unfinished).

909    pub unfinished: bool,
910}
912fn kind_words(kind: Kind) -> &'static str {
913    match kind {
914        Kind::Noul => "a yes or no",
915        Kind::Choice => "a pick",
916        Kind::Score => "a how much",
917        Kind::Chance => "a how likely",
918    }
919}

The line under the box: what Jev makes of the text so far.

922pub fn live(glance: Option<Glance>) -> Markup {
923    html! {
924        p #live .live aria-live="polite" {
925            @if let Some(glance) = glance {
926                @if glance.unfinished {
927                    // Half a question is not judged yet: what Jev would say
928                    // of it is not what it will say of the whole.
929                    span .dot {} "Jev reads this as unfinished · keep typing"
930                } @else if ask::is_yes(glance.answerable) {
931                    span .dot.yes {} "Jev can judge this"
932                    @if let Some(kind) = glance.kind { " · " (kind_words(kind)) }
933                } @else {
934                    span .dot {} "Jev can't judge this yet"
935                }
936                @if !glance.unfinished { span .meta { " · " (probability(glance.answerable)) } }
937                // The way forward, so a low score mid-sentence does not end the
938                // typing: asking shows where an LLM can answer (see meatproxy.rs).
939                // Short enough for one line beside the pixel font at desktop width.
940                @if !glance.unfinished && !ask::is_yes(glance.answerable) { span .meta { " · press Ask for an LLM that can" } }
941            }
942        }
943    }
944}

Under the box as the visitor types: questions already on the feed that what is typed could be the start of, each with Jev's answer. Following one asks it again. Empty when there are none.

949pub fn suggested(entries: &[Entry]) -> Markup {
950    html! {
951        ul #suggested .feed.suggested aria-label="Already asked" {
952            @for asked in entries { (entry(asked, None)) }
953        }
954    }
955}

The transcript before anything is asked.

958fn resting() -> Markup {
959    html! { div #transcript {} }
960}

The transcript as it stands. Sent whole each time; Datastar morphs it in by id.

963pub fn transcript(view: &View) -> Markup {
964    let results = view.results();
965    let said = view.ending.is_some() || !results.is_empty();
966    html! {
967        div #transcript {
968            div .sheet {
969                div .wrap {
970                    p .step { (step(view)) }
971                    div .win.white {
972                        div .bar-title { (icon(&icons::JEV, "")) "Jev" }
973                        div .body {
974                            div .row { span .who { "Q:" } span .q { (view.input) } }
975                            @if said {
976                                div .row { span .who { "Jev:" } div .said {
977                                    @for shown in &results { (result(shown)) }
978                                    @if let Some(ending) = &view.ending { (ended(ending, &view.input, view.preview.as_ref())) }
979                                } }
980                            }
981                            // Filled in once the answer, or the declining, is kept
982                            // (`/rate`); the thumbs mean something else on the latter.
983                            @if matches!(view.ending, Some(Ending::Answered | Ending::NotAQuestion { .. } | Ending::NoQuestion)) {
984                                div #rating .rating {}
985                                (comment_box(!matches!(view.ending, Some(Ending::Answered))))
986                            }
987                            @if matches!(view.ending, Some(Ending::Answered | Ending::NotAQuestion { .. })) {
988                                (share(&view.input))
989                            }
990                        }
991                    }
992                }
993            }
994            @if view.facts.is_some() {
995                div .band {
996                    div .wrap.stack {
997                        (rules_card(view))
998                        @if let Some(facts) = &view.facts { (jev_card(facts)) }
999                        @if let Some(llm) = &view.llm { (llm_card(llm)) }
1000                        @for tool in &view.tools { (tool_card(tool, view.judging.is_some())) }
1001                        @if let Some(judging) = &view.judging { (jev_card(judging)) }
1002                    }
1003                }
1004            }
1005        }
1006    }
1007}
1009fn step(view: &View) -> &'static str {
1010    match &view.ending {
1011        Some(Ending::Empty) => "Type a question first",
1012        Some(Ending::Answered | Ending::NotAQuestion { .. }) => "Was that so hard?",
1013        Some(_) => "Nothing came back",
1014        None if view.judging.is_some() => "Step 3: asking Jev",
1015        None if view.llm.is_some() => "Step 2: an LLM is writing Jev's question",
1016        None if view.facts.is_some() => "Step 1: asking Jev what kind of question that is",
1017        None => "Getting Jev",
1018    }
1019}

The rules as a Rete network, with what is known about this query marked on it (diagram.rs says what each mark means).

1023fn rules_card(view: &View) -> Markup {
1024    let network = Network::lmjtfy();
1025    html! {
1026        details .win open {
1027            summary .bar-title {
1028                span .fold {} (icon(&icons::TOOL, "")) "rules · rete"
1029                span .meta {
1030                    // No rule decides a step when Jev's first answers settle
1031                    // it: then the rules that hold are the story.
1032                    @let holding = network.holding(&view.known);
1033                    @if !view.fired.is_empty() {
1034                        (diagram::sequence(&network, &view.fired))
1035                    } @else if holding.is_empty() {
1036                        "waiting on facts"
1037                    } @else {
1038                        (holding.join(" · "))
1039                    }
1040                }
1041            }
1042            div .body {
1043                (diagram::rete(&network, &view.known, &view.fired, |fact| view.shown(fact), |_| None))
1044                p .fine {
1045                    "Facts on the left, with what Jev said; rules on the right, numbered in the order they "
1046                    "decided; the tests that join them between. Filled is what a rule that fired stood on, "
1047                    "outlined was learned and not needed, dashed failed. A line from a rule back to a fact "
1048                    "is a call that teaches it. "
1049                    a href="/rules" { "Try the rules yourself." }
1050                }
1051            }
1052        }
1053    }
1054}

What Jev said to one question.

1057fn result(shown: &Shown<'_>) -> Markup {
1058    html! {
1059        div .result {
1060            p .asked {
1061                (shown.question)
1062                @if let Some(read_as) = shown.read_as { span .read-as { " · " (read_as) } }
1063            }
1064            @match shown.judged {
1065                // The probability is the answer; the note says it is a Noul,
1066                // and how to read the number.
1067                Judged::Noul(_) if shown.chance => {
1068                    p .answer { (shown.headline()) }
1069                    p .verdict { (shown.told().note().unwrap_or_default()) }
1070                },
1071                Judged::Noul(p_yes) => {
1072                    p .answer { (headline(shown.judged)) }
1073                    p .verdict { "Noul · p(yes) = " (probability(*p_yes)) }
1074                },
1075                Judged::Choice(answer) => (choice(answer)),
1076                Judged::Score(answer) => (score(answer)),
1077            }
1078        }
1079    }
1080}

A Noul as a word: flat when Jev is sure, a question when it is not.

1083fn yes_or_no(p_yes: f64) -> &'static str {
1084    match (ask::is_yes(p_yes), ask::is_sure(p_yes)) {
1085        (true, true) => "Yes.",
1086        (true, false) => "Yes?",
1087        (false, true) => "No.",
1088        (false, false) => "No?",
1089    }
1090}

The level a Score fell nearest, and the highest level there is. The score is Jev's answer: the probability-weighted level, which "can land between levels" (TypeSafe's docs), so this is the level it rounds to.

1095fn nearest(answer: &ScoreAnswer) -> (usize, usize) {
1096    let top = answer.probabilities.len().saturating_sub(1);
1097    ((answer.score.round().max(0.0) as usize).min(top), top)
1098}

The level with the most probability (the lowest of equals). It can differ from the nearest: a spread-out answer has a mean on a level that is not the likeliest, which is what a low confidence says.

1103fn likeliest(answer: &ScoreAnswer) -> usize {
1104    answer.probabilities.iter().enumerate().fold((0, f64::NEG_INFINITY), |best, (level, p)| if *p > best.1 { (level, *p) } else { best }).0
1105}

An answer in a few words: what the transcript prints large, and what the feed prints beside the question.

1109fn headline(judged: &Judged) -> String {
1110    match judged {
1111        Judged::Noul(p_yes) => yes_or_no(*p_yes).to_owned(),
1112        Judged::Choice(answer) => format!("{}.", answer.choice),
1113        Judged::Score(answer) => {
1114            let level = answer.legend.get(nearest(answer).0).map(String::as_str).unwrap_or_default();
1115            format!("{level}{}", if ask::is_confident(answer.confidence) { "." } else { "?" })
1116        }
1117    }
1118}

Where the square sits on its rule, from a probability.

1121fn at(p: f64) -> String {
1122    format!("left: {:.1}%", (p * 100.0).clamp(0.0, 100.0))
1123}
1125fn percent(p: f64) -> String {
1126    format!("{:.0}%", p * 100.0)
1127}

Every option with its probability, most likely first.

1130fn choice(answer: &ChoiceAnswer) -> Markup {
1131    let mut ranked: Vec<&(String, f64)> = answer.probabilities.iter().collect();
1132    ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
1133    html! {
1134        p .answer.pick { (headline(&Judged::Choice(answer.clone()))) }
1135        div .bars {
1136            @for (label, p) in ranked {
1137                div .bar.won[*label == answer.choice] {
1138                    span .label { (label) }
1139                    span .pct { (percent(*p)) }
1140                    span .track { span .thumb style=(at(*p)) {} }
1141                }
1142            }
1143        }
1144        p .verdict { "Choice · confidence " (probability(answer.confidence)) }
1145    }
1146}

Every level with its probability, lowest first. The headline's level (the one nearest the score) is inked; the likeliest is tagged, and is the same level when Jev is sure. Under the bars: the score, which is the mean, and how sure Jev is.

1152fn score(answer: &ScoreAnswer) -> Markup {
1153    let (nearest, top) = nearest(answer);
1154    let likeliest = likeliest(answer);
1155    let sure = ask::is_confident(answer.confidence);
1156    html! {
1157        p .answer.pick {
1158            (headline(&Judged::Score(answer.clone())))
1159            span .of { " mean " (format!("{:.1}", answer.score)) " / " (top) }
1160        }
1161        div .bars {
1162            @for (level, p) in answer.probabilities.iter().enumerate() {
1163                div .bar.won[level == nearest] {
1164                    span .label {
1165                        (level) " · " (answer.legend.get(level).map(String::as_str).unwrap_or_default())
1166                        @if level == likeliest { " " span .tag { "likeliest" } }
1167                    }
1168                    span .pct { (percent(*p)) }
1169                    span .track { span .thumb style=(at(*p)) {} }
1170                }
1171            }
1172        }
1173        p .verdict {
1174            "Score · 0 to " (top) " · confidence " (probability(answer.confidence))
1175            @if !sure { " · unsure: the levels are ambiguous" }
1176        }
1177    }
1178}
1180fn ended(ending: &Ending, input: &str, preview: Option<&meatproxy::Head>) -> Markup {
1181    match ending {
1182        Ending::Answered => html! {},
1183        Ending::Slow => html! {
1184            p .answer.quiet { "Slow down." }
1185            p .why { "That is a lot of questions in one minute. Nothing was sent. Try again in a minute." }
1186        },
1187        Ending::NotAQuestion { p_yes } => html! {
1188            p .answer { (yes_or_no(*p_yes)) }
1189            p .why { "That's not a question I can answer. Ask me yes or no, which one, or how much." }
1190            (meatproxy::suggestion(input, preview))
1191            p .verdict { "p(I can answer this) = " (probability(*p_yes)) }
1192        },
1193        Ending::NoQuestion => html! {
1194            p .answer.quiet { "Jev wasn't asked anything." }
1195            p .why { "The LLM didn't write a question Jev can take. Its reply is below." }
1196            (meatproxy::suggestion(input, preview))
1197        },
1198        Ending::Failed { error } => html! {
1199            p .answer.quiet { "Jev didn't answer." }
1200            p .why { (error) }
1201        },
1202        Ending::Offline => html! {
1203            p .answer.quiet { "Jev is offline." }
1204            p .why { "This server has no TypeSafe API key, so nothing was sent." }
1205        },
1206        Ending::NotKept { id } => html! {
1207            p .answer.quiet { "Not asked yet." }
1208            p .why {
1209                "This version leads to a question Jev was never asked. Looking back costs nothing, so it was not sent. "
1210                (again(id, "↻ Ask it"))
1211            }
1212        },
1213        Ending::Spent { what } => html! {
1214            p .answer.quiet { "Out of budget for today." }
1215            p .why { "Everyone shares one daily budget for " (what) ", and it is used up. It resets at 00:00 UTC." }
1216        },
1217        Ending::Empty => html! { p .answer.quiet { "Ask me something." } },
1218    }
1219}

The LLM's call: what it was sent and what it wrote.

1222fn llm_card(made: &LlmCall) -> Markup {
1223    html! {
1224        details .win {
1225            summary .bar-title {
1226                span .fold {} (icon(&icons::LLM, "")) "llm · " (made.model)
1227                span .meta {
1228                    @match &made.outcome {
1229                        None => "writing Jev's question",
1230                        Some(LlmOutcome { sent: Sent::Before { at_ms }, .. }) => (kept(*at_ms)),
1231                        Some(outcome) => (format!("{} ms · {:.1} neurons", outcome.took.as_millis(), outcome.neurons)),
1232                    }
1233                }
1234            }
1235            div .body {
1236                (versions(made.kept.as_ref()))
1237                div .wire {
1238                    span .label-s { "request" }
1239                    (json(&made.request))
1240                }
1241                @if let Some(outcome) = &made.outcome {
1242                    div .wire {
1243                        span .label-s { "response" }
1244                        (json(&outcome.body))
1245                        @if outcome.dropped > 0 {
1246                            p .note { (outcome.dropped) " more tool calls were over the limit and were not run." }
1247                        }
1248                    }
1249                }
1250            }
1251        }
1252    }
1253}

One tool call the LLM made: what it wrote, and whether it went to Jev. sent is whether a request to Jev was made at all.

1257fn tool_card(tool: &Tool, sent: bool) -> Markup {
1258    html! {
1259        details .win open[tool.problem().is_some()] {
1260            summary .bar-title {
1261                span .fold {} (icon(&icons::TOOL, "")) "tool call · " (tool.call.name)
1262                span .meta {
1263                    @if tool.problem().is_some() { "not sent" } @else if sent { "sent to Jev as " (tool.id) } @else { (tool.id) }
1264                }
1265            }
1266            div .body {
1267                div .wire {
1268                    span .label-s { "the LLM's tool call" }
1269                    pre { (tool.call.name) }
1270                    (json(&tool.call.arguments))
1271                    @if let Some(problem) = tool.problem() {
1272                        p .note { "Not sent to Jev: " (problem) }
1273                    }
1274                }
1275            }
1276        }
1277    }
1278}

A request to Jev: the body as sent and the response as it arrived. ↻ for the call id: send its request again, and work the rest out from the answer.

1283fn again(id: &str, label: &str) -> Markup {
1284    html! {
1285        button .again type="button" title="Send this request again and keep the answer as its newest"
1286            "data-on:click"={ "$pins = ($pins || '') + ';" (id) ":fresh'; @post('/ask')" } { (label) }
1287    }
1288}

◀ version n of m ▶ and ↻, for a call that has an answer.

1291fn versions(kept: Option<&Versions>) -> Markup {
1292    let Some(kept) = kept else { return html! {} };
1293    let step = |to: u32, glyph: &str, label: &str| {
1294        html! {
1295            button .step type="button" title=(label)
1296                "data-on:click"={ "$pins = ($pins || '') + ';" (kept.id) ":" (to) "'; @post('/ask')" } { (glyph) }
1297        }
1298    };
1299    html! {
1300        div .versions {
1301            @if kept.version > 1 { (step(kept.version - 1, "◀", "The answer before")) }
1302            span .label-s { "answer " (kept.version) " of " (kept.versions) }
1303            @if kept.version < kept.versions { (step(kept.version + 1, "▶", "The answer after")) }
1304            (again(&kept.id, "↻ Ask again"))
1305        }
1306    }
1307}
1309fn jev_card(made: &JevCall) -> Markup {
1310    let questions = if made.parts.len() == 1 { "1 question".to_owned() } else { format!("{} questions", made.parts.len()) };
1311    html! {
1312        details .win open {
1313            summary .bar-title {
1314                span .fold {} (icon(&icons::TOOL, "")) "jev · " (made.id) " · " (questions)
1315                span .meta { (meta(made.outcome.as_ref())) }
1316            }
1317            div .body {
1318                (versions(made.kept.as_ref()))
1319                div .wire {
1320                    span .label-s { "request" }
1321                    pre {
1322                        "POST " (jev_protocol::ENDPOINT) "\n"
1323                        "authorization: Bearer [the server's key]\n"
1324                        "content-type: application/json"
1325                    }
1326                    (json(&made.request))
1327                }
1328                @match &made.outcome {
1329                    Some(Outcome::Answered { body, .. }) => div .wire {
1330                        span .label-s { "response" }
1331                        (json(body))
1332                    },
1333                    Some(Outcome::Failed { error, .. }) => div .wire {
1334                        span .label-s { "no response" }
1335                        pre { (error) }
1336                    },
1337                    None => {},
1338                }
1339            }
1340        }
1341    }
1342}

A JSON body, indented and coloured for reading. Only whitespace is added: keys keep their order and every value is printed as it was sent. Text that is not JSON is shown as it is.

1347fn json(text: &str) -> Markup {
1348    match serde_json::from_str::<Value>(text) {
1349        Ok(value) => html! { pre .json { (node(&value, 0)) } },
1350        Err(_) => html! { pre { (text) } },
1351    }
1352}
1354fn node(value: &Value, depth: usize) -> Markup {
1355    let indent = "  ".repeat(depth + 1);
1356    let close = "  ".repeat(depth);
1357    match value {
1358        Value::Object(map) if !map.is_empty() => html! {
1359            "{\n"
1360            @for (i, (key, child)) in map.iter().enumerate() {
1361                (indent) span .k { (quoted(key)) } ": " (node(child, depth + 1))
1362                @if i + 1 < map.len() { "," } "\n"
1363            }
1364            (close) "}"
1365        },
1366        Value::Array(items) if !items.is_empty() => html! {
1367            "[\n"
1368            @for (i, child) in items.iter().enumerate() {
1369                (indent) (node(child, depth + 1))
1370                @if i + 1 < items.len() { "," } "\n"
1371            }
1372            (close) "]"
1373        },
1374        Value::String(text) => html! { span .s { (quoted(text)) } },
1375        // Numbers, booleans, null, and the empty object and array.
1376        other => html! { span .n { (other.to_string()) } },
1377    }
1378}

A string as JSON writes it, quotes and escapes included.

1381fn quoted(text: &str) -> String {
1382    Value::String(text.to_owned()).to_string()
1383}

A call's title bar when nothing was sent: the response is the one kept from the first time this exact request was made.

1387fn kept(at_ms: f64) -> String {
1388    format!("not sent · answered {}", when(at_ms))
1389}
1391fn meta(outcome: Option<&Outcome>) -> String {
1392    match outcome {
1393        None => "waiting for Jev".to_owned(),
1394        Some(Outcome::Answered { sent: Sent::Before { at_ms }, request_id, .. }) => {
1395            let mut parts = vec![kept(*at_ms)];
1396            parts.extend(request_id.clone());
1397            parts.join(" · ")
1398        }
1399        Some(Outcome::Answered { request_id, attempts, usage, took, .. }) => {
1400            let mut parts = vec![
1401                format!("{} ms", took.as_millis()),
1402                if *attempts == 1 { "1 attempt".to_owned() } else { format!("{attempts} attempts") },
1403                format!("{} tokens in", usage.input_tokens),
1404                format!("${:.6}", usage.dollars()),
1405            ];
1406            parts.extend(request_id.clone());
1407            parts.join(" · ")
1408        }
1409        Some(Outcome::Failed { request_id, took, .. }) => {
1410            let mut parts = vec!["failed".to_owned(), format!("{} ms", took.as_millis())];
1411            parts.extend(request_id.clone());
1412            parts.join(" · ")
1413        }
1414    }
1415}
1416
1417fn share(input: &str) -> Markup {
1418    let query: String = url_encode(input);
1419    html! {
1420        div .share {
1421            span { "Send it to them:" }
1422            a #link href={ "/?q=" (query) } { "/?q=" (query) }
1423            button .ghost type="button" data-copy="link" { "Copy link" }
1424        }
1425    }
1426}

Percent-encodes everything but unreserved characters, for a query value. text percent-encoded, for a URL or a header.

1430pub fn url_encoded(text: &str) -> String {
1431    url_encode(text)
1432}
1434fn url_encode(text: &str) -> String {
1435    let mut out = String::with_capacity(text.len());
1436    for byte in text.bytes() {
1437        match byte {
1438            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(byte as char),
1439            b' ' => out.push('+'),
1440            _ => out.push_str(&format!("%{byte:02X}")),
1441        }
1442    }
1443    out
1444}
1445
1446fn probability(p: f64) -> String {
1447    format!("{p:.2}")
1448}
1449
1450#[cfg(test)]
1451mod tests;