lmjtfy.git / apps / lmjtfy / src / view.rs
view.rsannotatedview.rssource1315 lines · 51.2 KB · raw

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

3use crate::diagram;
5pub mod code;
6pub mod toast;
7
8use std::time::Duration;
9
10use archive::{Answer, Detail, Entry, Home, Stats, when};
11use ask::{Judged, Outcome, Prepared, Sent};
12use jev_protocol::{ChoiceAnswer, ScoreAnswer};
13use llm::{Draft, ToolCall};
14use maud::{DOCTYPE, Markup, PreEscaped, html};
15use rules::{Fact, Kind, Known, Network};
16use serde_json::Value;
17
18const CSS: &str = include_str!("page.css");
19const 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.

22const 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.

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

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

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

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

59pub struct JevCall {

What the request is for: facts or answers.

61    pub id: &'static str,

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

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

The request body as sent.

65    pub request: String,

None while Jev is still answering.

67    pub outcome: Option<Outcome>,

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

69    pub kept: Option<Versions>,
70}

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).

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

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

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

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

103pub struct LlmCall {
104    pub model: &'static str,

The request body as sent.

106    pub request: String,

None while the LLM is still writing.

108    pub outcome: Option<LlmOutcome>,

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

110    pub kept: Option<Versions>,
111}
113pub struct LlmOutcome {

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

115    pub body: String,

What the reply cost when it was sent.

117    pub neurons: f64,
118    pub took: Duration,

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

120    pub dropped: usize,

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

123    pub sent: Sent,
124}

One tool call the LLM made.

127pub struct Tool {

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

129    pub id: String,
130    pub call: ToolCall,

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

132    pub refused: Option<String>,
133}
135impl Tool {

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

137    pub fn draft(&self) -> Option<&Draft> {
138        self.call.draft.as_ref().ok().filter(|_| self.refused.is_none())
139    }

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

142    fn problem(&self) -> Option<&str> {
143        match &self.call.draft {
144            Err(error) => Some(error),
145            Ok(_) => self.refused.as_deref(),
146        }
147    }
148}

How the exchange ended.

151pub enum Ending {

Nothing was typed; nothing was sent.

153    Empty,

The gate said this is not something Jev can judge.

155    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.

158    Answered,

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

160    Slow,

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

162    NoQuestion,

A call failed, or could not be built.

164    Failed { error: String },

The Worker has no API key.

166    Offline,

Today's shared budget for what is used up.

168    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 ↻.

171    NotKept { id: String },
172}

One answer in the transcript.

175struct Shown<'a> {

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

177    question: &'a str,
178    judged: &'a Judged,

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

180    read_as: Option<&'static str>,

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

182    chance: bool,
183}
185impl Shown<'_> {

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

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

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

206    fn headline(&self) -> String {
207        match self.judged {
208            Judged::Noul(p) if self.chance => format!("{}.", percent(*p)),
209            judged => headline(judged),
210        }
211    }
212}

Everything the transcript shows.

215pub struct View {
216    pub input: String,

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

218    pub known: Known,

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

221    pub fired: Vec<usize>,

The request for the facts the rules wanted.

223    pub facts: Option<JevCall>,
224    pub llm: Option<LlmCall>,
225    pub tools: Vec<Tool>,

The request that asks Jev every question the LLM wrote.

227    pub judging: Option<JevCall>,

None while calls are still being made.

229    pub ending: Option<Ending>,

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

231    pub pins: archive::Pins,
232}
234impl View {
235    pub fn new(input: String) -> Self {
236        View {
237            input,
238            known: Known::default(),
239            fired: Vec::new(),
240            facts: None,
241            llm: None,
242            tools: Vec::new(),
243            judging: None,
244            ending: None,
245            pins: archive::Pins::default(),
246        }
247    }

$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.

251    pub fn pinned(&self) -> String {
252        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())];
253        let pins: Vec<String> = kept.into_iter().flatten().filter(|kept| kept.version < kept.versions).map(|kept| format!("{}:{}", kept.id, kept.version)).collect();
254        pins.join(";")
255    }

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

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

The kind Jev thinks the input most likely is.

284    pub fn reading(&self) -> Option<Kind> {
285        match self.fact(Fact::Reads(Kind::Noul))? {
286            Judged::Choice(answer) => ask::readings(answer).first().copied(),
287            _ => None,
288        }
289    }

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

293    fn shown(&self, fact: Fact) -> Option<String> {
294        match fact {
295            Fact::Drafted => Some(self.tools.iter().filter(|tool| tool.draft().is_some()).count().to_string()),
296            Fact::Judged => Some(self.tools.iter().filter(|tool| self.judged(tool).is_some()).count().to_string()),
297            Fact::Reads(kind) => match self.fact(fact)? {
298                Judged::Choice(answer) => ask::kind_probability(answer, kind).map(probability),
299                _ => None,
300            },
301            _ => match self.fact(fact)? {
302                Judged::Noul(p_yes) => Some(probability(*p_yes)),
303                Judged::Score(answer) => Some(format!("{:.1}", answer.score)),
304                Judged::Choice(_) => None,
305            },
306        }
307    }

Whether Jev answered any question the LLM wrote.

310    pub fn any_judged(&self) -> bool {
311        self.tools.iter().any(|tool| self.judged(tool).is_some())
312    }

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.

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

Whether there is any answer to show.

342    pub fn answered(&self) -> bool {
343        !self.results().is_empty()
344    }

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

348    pub fn said(&self) -> Vec<Answer> {
349        self.results().iter().map(Shown::told).collect()
350    }
351}

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

354pub struct Shared {
355    pub status: budget::Status,
356    pub neurons_per_day: f64,
357    pub jev_dollars_per_day: f64,
358}

A whole number with thousands separators.

361fn grouped(n: f64) -> String {
362    let digits = format!("{:.0}", n.max(0.0));
363    let mut out = String::new();
364    for (i, digit) in digits.chars().enumerate() {
365        if i > 0 && (digits.len() - i) % 3 == 0 {
366            out.push(',');
367        }
368        out.push(digit);
369    }
370    out
371}

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.

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

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.

414fn entry(entry: &Entry, id: Option<String>) -> Markup {
415    html! {
416        li .entry id=[id] {
417            a href={ "/?q=" (url_encode(&entry.input)) } title=(when(entry.asked_ms)) { (entry.input) }
418            span .said-s {
419                (entry.headlines())
420                @if entry.times > 1 { span .times { " ×" (entry.times) } }
421            }
422        }
423    }
424}
426fn lately_entry(asked: &Entry) -> Markup {
427    entry(asked, Some(entry_id(&asked.input)))
428}

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.

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

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.

441fn more(shown: &[Entry]) -> Markup {
442    let after = (shown.len() == archive::FEED as usize).then(|| shown.last()).flatten();
443    html! {
444        @if let Some(last) = after {
445            li #more "data-on-intersect__once"={
446                "@get('/feed?ms=" (last.asked_ms) "&q=" (url_encode(&last.input)) "')"
447            } { span .meta { "more…" } }
448        } @else {
449            li #more hidden {}
450        }
451    }
452}

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

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

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

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

The most asked questions, replaced whole when they change.

466pub fn most(most: &[Entry]) -> Markup {
467    html! {
468        div #most {
469            @if !most.is_empty() {
470                span .label-s { "most asked" }
471                ol .feed { @for asked in most { (entry(asked, None)) } }
472            }
473        }
474    }
475}

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.

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

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

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

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).

541fn mark(text: &str) -> String {
542    // FNV-1a.
543    let hash = text.bytes().fold(0x811c_9dc5_u32, |hash, byte| (hash ^ u32::from(byte)).wrapping_mul(0x0100_0193));
544    format!("{hash:08x}")
545}

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.

552fn preview(typed: &str, said: Option<&Entry>, origin: &str) -> Markup {
553    // Each answer with its numbers: the probability, the confidence, the
554    // likeliest options. That is half of what Jev says.
555    let answer = said.filter(|entry| !entry.answers.is_empty()).map(|entry| {
556        let lines: Vec<String> = entry.answers.iter().map(Answer::line).collect();
557        lines.join("  /  ")
558    });
559    let (title, description, picture) = if typed.is_empty() {
560        (
561            "Let Me Jev That For You".to_owned(),
562            "Ask Jev anything. It answers yes, no, which one or how much, with a probability, and shows its work."
563                .to_owned(),
564            format!("{origin}/card.png"),
565        )
566    } else {
567        let description = match &answer {
568            Some(answer) => format!("Jev: {answer}"),
569            None => "Let me Jev that for you.".to_owned(),
570        };
571        let picture =
572            format!("{origin}/card.png?q={}&a={}", url_encode(typed), mark(answer.as_deref().unwrap_or_default()));
573        (format!("Q: {typed}"), description, picture)
574    };
575    html! {
576        title { (title) }
577        meta name="description" content=(description);
578        meta property="og:type" content="website";
579        meta property="og:site_name" content="LMJTFY · Let Me Jev That For You";
580        meta property="og:title" content=(title);
581        meta property="og:description" content=(description);
582        meta property="og:image" content=(picture);
583        meta property="og:image:type" content="image/png";
584        meta property="og:image:width" content=(card::WIDTH);
585        meta property="og:image:height" content=(card::HEIGHT);
586        // The picture is shown large, under the text.
587        meta name="twitter:card" content="summary_large_image";
588        // The bar down the side of the embed: the page's pink.
589        meta name="theme-color" content="#f386a1";
590    }
591}

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

594fn head(preview: Markup) -> Markup {
595    html! {
596        head {
597            meta charset="utf-8";
598            meta name="viewport" content="width=device-width, initial-scale=1";
599            // The build this page came from, for `/live` to compare (page.js).
600            meta name="lmjtfy-build" content=(crate::BUILD);
601            (preview)
602            link rel="preconnect" href="https://fonts.googleapis.com";
603            link rel="preconnect" href="https://fonts.gstatic.com" crossorigin;
604            link rel="stylesheet" href=(FONTS);
605            style { (PreEscaped(CSS)) }
606        }
607    }
608}
610fn nav() -> Markup {
611    html! {
612        nav {
613            a href="/" { "LMJTFY" }
614            // Filled in over `/live` (page.js): pages open now, and a toast
615            // for each question asked and each clone while this one is open.
616            span .online-wrap tabindex="0" {
617                span #online .online hidden {}
618                (places(&[]))
619            }
620            div #toasts aria-live="polite" {}
621            a .dark href=(CODE) { "Code" }
622            a .dark href="https://docs.typesafe.ai" { "What is Jev?" }
623        }
624    }
625}

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.

630pub fn rating(rating: Option<&archive::Rating>, can: bool) -> Markup {
631    let button = |vote: archive::Vote, glyph: &str, n: u32, label: &str| {
632        let on = rating.and_then(|rating| rating.mine) == Some(vote);
633        let name = match vote {
634            archive::Vote::Up => "up",
635            archive::Vote::Down => "down",
636        };
637        html! {
638            button .vote .on[on] type="button" disabled[!can] aria-pressed=(on) title=(label)
639                "data-on:click"={ "$vote = '" (name) "'; @post('/rate')" } {
640                span .glyph aria-hidden="true" { (glyph) } " " (n)
641            }
642        }
643    };
644    html! {
645        div #rating .rating {
646            @if let Some(shown) = rating {
647                span .label-s { "Was Jev right?" }
648                (button(archive::Vote::Up, "👍", shown.up, "Jev was right"))
649                (button(archive::Vote::Down, "👎", shown.down, "Jev was wrong"))
650                @if !can { span .meta { "Voting needs this site's cookie: reload the page." } }
651            }
652        }
653    }
654}

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.

659pub fn places(open: &[(archive::Place, u32)]) -> Markup {
660    html! {
661        ul #places .places {
662            @for (place, n) in open {
663                li {
664                    span .flag aria-hidden="true" { (place.flag().unwrap_or_else(|| "·".to_owned())) }
665                    span .where { (place.label()) }
666                    @if *n > 1 { span .times { "×" (n) } }
667                }
668            }
669        }
670    }
671}

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

674const 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).

678fn footer(clone: Option<&str>) -> Markup {
679    html! {
680        footer {
681            div .wrap {
682                div .about {
683                    (icon(&icons::JEV, "big"))
684                    span {
685                        "Jev answers typed questions with calibrated probabilities, not text. "
686                        "Rules decide what to ask it, and an LLM writes the options when a question needs them. "
687                        a href="/rules" { "See the rules." } " "
688                        "Not affiliated with TypeSafe AI."
689                    }
690                }
691                @if let Some(origin) = clone {
692                    (clone_line(&crate::clone::command(origin), html! { a href=(CODE) { "Get the code:" } }))
693                }
694            }
695        }
696    }
697}

A clone command with a copy button.

700fn clone_line(command: &str, label: Markup) -> Markup {
701    html! {
702        div .share.clone {
703            span { (label) }
704            code #clone { (command) }
705            button .ghost type="button" data-copy="clone" { "Copy" }
706        }
707    }
708}

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

711pub fn playground(
712    network: &Network,
713    known: &Known,
714    fired: &[usize],
715    says: &str,
716    link: impl Fn(diagram::Clicked) -> String,
717) -> Markup {
718    let title = "The rules · LMJTFY";
719    let about = "The rules that decide what happens to a question, as a Rete network you can set the facts of.";
720    html! {
721        (DOCTYPE)
722        html lang="en" {
723            (head(html! {
724                title { (title) }
725                meta name="description" content=(about);
726                meta property="og:title" content=(title);
727                meta property="og:description" content=(about);
728                meta name="theme-color" content="#f386a1";
729            }))
730            body {
731                div .sheet {
732                    div .wrap {
733                        (nav())
734                        div .stack {
735                            div .win {
736                                div .bar-title {
737                                    (icon(&icons::TOOL, "")) "rules · rete"
738                                    span .meta {
739                                        @if fired.is_empty() { "nothing decided yet" } @else { (diagram::sequence(network, fired)) }
740                                    }
741                                }
742                                div .body {
743                                    (diagram::rete(network, known, fired, |_| None, |clicked| Some(link(clicked))))
744                                    p .says { (says) }
745                                    p .fine {
746                                        "Click a test to say its fact is so, or a fact to step through what it can be. "
747                                        "Nothing is asked of Jev or the LLM here: the facts are whatever you set. "
748                                        a href="/rules" { "Start over." }
749                                    }
750                                    p .fine {
751                                        "On a real question, Jev gives every fact in the first frame in one "
752                                        "request, and the rules take it from there. Filled is what a rule that "
753                                        "holds stands on, outlined is known and not needed, dashed failed."
754                                    }
755                                }
756                            }
757                        }
758                    }
759                }
760                // The online count and the toasts, as on every page.
761                script { (PreEscaped(JS)) }
762            }
763        }
764    }
765}

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

768pub fn page(typed: &str, said: Option<&Entry>, origin: &str, shared: Option<Shared>, home: Option<&Home>) -> Markup {
769    html! {
770        (DOCTYPE)
771        html lang="en" {
772            (head(html! {
773                (preview(typed, said, origin))
774                script type="module" src="/datastar.js" {}
775            }))
776            body data-signals="{q: '', pins: ''}" {
777                div .sheet {
778                    div .wrap {
779                        (nav())
780                        div .headline {
781                            p .eyebrow { "Let me ........................ Jev that for you" }
782                            h1 { "Let Me Jev That For You" }
783                        }
784                        div .stack {
785                            div .win {
786                                div .bar-title { (icon(&icons::ASK, "")) "Ask" }
787                                div .body {
788                                    form #ask data-type=(typed) data-on:submit__prevent="@post('/ask')" data-indicator:asking {
789                                        // Jev answers in about a fifth of a second, so the
790                                        // gate runs on every pause in the typing.
791                                        input #q name="q" data-bind:q "data-on:input__debounce.300ms"="@post('/gate')"
792                                            autocomplete="off" required maxlength=(ask::MAX_INPUT_CHARS)
793                                            placeholder="Ask Jev anything" aria-label="Your question";
794                                        button type="submit" data-attr:disabled="$asking" { "Ask Jev" }
795                                    }
796                                    (live(None))
797                                }
798                            }
799                            (budget(shared))
800                            (feed(home))
801                            (tally(home.map(|home| &home.stats)))
802                        }
803                    }
804                }
805                (resting())
806                (footer(Some(origin)))
807                script { (PreEscaped(JS)) }
808            }
809        }
810    }
811}

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

814pub struct Glance {

Jev's probability that it can judge the text.

816    pub answerable: f64,
817    pub kind: Option<Kind>,
818}
820fn kind_words(kind: Kind) -> &'static str {
821    match kind {
822        Kind::Noul => "a yes or no",
823        Kind::Choice => "a pick",
824        Kind::Score => "a how much",
825        Kind::Chance => "a how likely",
826    }
827}

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

830pub fn live(glance: Option<Glance>) -> Markup {
831    html! {
832        p #live .live aria-live="polite" {
833            @if let Some(glance) = glance {
834                @if ask::is_yes(glance.answerable) {
835                    span .dot.yes {} "Jev can judge this"
836                    @if let Some(kind) = glance.kind { " · " (kind_words(kind)) }
837                } @else {
838                    span .dot {} "Jev can't judge this yet"
839                }
840                span .meta { " · " (probability(glance.answerable)) }
841            }
842        }
843    }
844}

The transcript before anything is asked.

847fn resting() -> Markup {
848    html! { div #transcript {} }
849}

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

852pub fn transcript(view: &View) -> Markup {
853    let results = view.results();
854    let said = view.ending.is_some() || !results.is_empty();
855    html! {
856        div #transcript {
857            div .sheet {
858                div .wrap {
859                    p .step { (step(view)) }
860                    div .win.white {
861                        div .bar-title { (icon(&icons::JEV, "")) "Jev" }
862                        div .body {
863                            div .row { span .who { "Q:" } span .q { (view.input) } }
864                            @if said {
865                                div .row { span .who { "Jev:" } div .said {
866                                    @for shown in &results { (result(shown)) }
867                                    @if let Some(ending) = &view.ending { (ended(ending)) }
868                                } }
869                            }
870                            @if matches!(view.ending, Some(Ending::Answered)) {
871                                // Filled in once the answer is kept (`/rate`).
872                                div #rating .rating {}
873                            }
874                            @if matches!(view.ending, Some(Ending::Answered | Ending::NotAQuestion { .. })) {
875                                (share(&view.input))
876                            }
877                        }
878                    }
879                }
880            }
881            @if view.facts.is_some() {
882                div .band {
883                    div .wrap.stack {
884                        (rules_card(view))
885                        @if let Some(facts) = &view.facts { (jev_card(facts)) }
886                        @if let Some(llm) = &view.llm { (llm_card(llm)) }
887                        @for tool in &view.tools { (tool_card(tool, view.judging.is_some())) }
888                        @if let Some(judging) = &view.judging { (jev_card(judging)) }
889                    }
890                }
891            }
892        }
893    }
894}
896fn step(view: &View) -> &'static str {
897    match &view.ending {
898        Some(Ending::Empty) => "Type a question first",
899        Some(Ending::Answered | Ending::NotAQuestion { .. }) => "Was that so hard?",
900        Some(_) => "Nothing came back",
901        None if view.judging.is_some() => "Step 3: asking Jev",
902        None if view.llm.is_some() => "Step 2: an LLM is writing Jev's question",
903        None if view.facts.is_some() => "Step 1: asking Jev what kind of question that is",
904        None => "Getting Jev",
905    }
906}

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

910fn rules_card(view: &View) -> Markup {
911    let network = Network::lmjtfy();
912    html! {
913        details .win open {
914            summary .bar-title {
915                span .fold {} (icon(&icons::TOOL, "")) "rules · rete"
916                span .meta {
917                    // No rule decides a step when Jev's first answers settle
918                    // it: then the rules that hold are the story.
919                    @let holding = network.holding(&view.known);
920                    @if !view.fired.is_empty() {
921                        (diagram::sequence(&network, &view.fired))
922                    } @else if holding.is_empty() {
923                        "waiting on facts"
924                    } @else {
925                        (holding.join(" · "))
926                    }
927                }
928            }
929            div .body {
930                (diagram::rete(&network, &view.known, &view.fired, |fact| view.shown(fact), |_| None))
931                p .fine {
932                    "Facts on the left, with what Jev said; rules on the right, numbered in the order they "
933                    "decided; the tests that join them between. Filled is what a rule that fired stood on, "
934                    "outlined was learned and not needed, dashed failed. A line from a rule back to a fact "
935                    "is a call that teaches it. "
936                    a href="/rules" { "Try the rules yourself." }
937                }
938            }
939        }
940    }
941}

What Jev said to one question.

944fn result(shown: &Shown<'_>) -> Markup {
945    html! {
946        div .result {
947            p .asked {
948                (shown.question)
949                @if let Some(read_as) = shown.read_as { span .read-as { " · " (read_as) } }
950            }
951            @match shown.judged {
952                // The probability is the answer; the note says it is a Noul,
953                // and how to read the number.
954                Judged::Noul(_) if shown.chance => {
955                    p .answer { (shown.headline()) }
956                    p .verdict { (shown.told().note().unwrap_or_default()) }
957                },
958                Judged::Noul(p_yes) => {
959                    p .answer { (headline(shown.judged)) }
960                    p .verdict { "Noul · p(yes) = " (probability(*p_yes)) }
961                },
962                Judged::Choice(answer) => (choice(answer)),
963                Judged::Score(answer) => (score(answer)),
964            }
965        }
966    }
967}

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

970fn yes_or_no(p_yes: f64) -> &'static str {
971    match (ask::is_yes(p_yes), ask::is_sure(p_yes)) {
972        (true, true) => "Yes.",
973        (true, false) => "Yes?",
974        (false, true) => "No.",
975        (false, false) => "No?",
976    }
977}

The level a Score fell nearest, and the highest level there is.

980fn nearest(answer: &ScoreAnswer) -> (usize, usize) {
981    let top = answer.probabilities.len().saturating_sub(1);
982    ((answer.score.round().max(0.0) as usize).min(top), top)
983}

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

987fn headline(judged: &Judged) -> String {
988    match judged {
989        Judged::Noul(p_yes) => yes_or_no(*p_yes).to_owned(),
990        Judged::Choice(answer) => format!("{}.", answer.choice),
991        Judged::Score(answer) => {
992            format!("{}.", answer.legend.get(nearest(answer).0).map(String::as_str).unwrap_or_default())
993        }
994    }
995}

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

998fn at(p: f64) -> String {
999    format!("left: {:.1}%", (p * 100.0).clamp(0.0, 100.0))
1000}
1002fn percent(p: f64) -> String {
1003    format!("{:.0}%", p * 100.0)
1004}

Every option with its probability, most likely first.

1007fn choice(answer: &ChoiceAnswer) -> Markup {
1008    let mut ranked: Vec<&(String, f64)> = answer.probabilities.iter().collect();
1009    ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
1010    html! {
1011        p .answer.pick { (headline(&Judged::Choice(answer.clone()))) }
1012        div .bars {
1013            @for (label, p) in ranked {
1014                div .bar.won[*label == answer.choice] {
1015                    span .label { (label) }
1016                    span .pct { (percent(*p)) }
1017                    span .track { span .thumb style=(at(*p)) {} }
1018                }
1019            }
1020        }
1021        p .verdict { "Choice · confidence " (probability(answer.confidence)) }
1022    }
1023}

Every level with its probability, lowest first, and where the score fell.

1026fn score(answer: &ScoreAnswer) -> Markup {
1027    let (nearest, top) = nearest(answer);
1028    html! {
1029        p .answer.pick {
1030            (headline(&Judged::Score(answer.clone())))
1031            span .of { " " (format!("{:.1}", answer.score)) " / " (top) }
1032        }
1033        div .bars {
1034            @for (level, p) in answer.probabilities.iter().enumerate() {
1035                div .bar.won[level == nearest] {
1036                    span .label { (level) " · " (answer.legend.get(level).map(String::as_str).unwrap_or_default()) }
1037                    span .pct { (percent(*p)) }
1038                    span .track { span .thumb style=(at(*p)) {} }
1039                }
1040            }
1041        }
1042        p .verdict { "Score · 0 to " (top) " · confidence " (probability(answer.confidence)) }
1043    }
1044}
1046fn ended(ending: &Ending) -> Markup {
1047    match ending {
1048        Ending::Answered => html! {},
1049        Ending::Slow => html! {
1050            p .answer.quiet { "Slow down." }
1051            p .why { "That is a lot of questions in one minute. Nothing was sent. Try again in a minute." }
1052        },
1053        Ending::NotAQuestion { p_yes } => html! {
1054            p .answer { (yes_or_no(*p_yes)) }
1055            p .why { "That's not a question I can answer. Ask me yes or no, which one, or how much." }
1056            p .verdict { "p(I can answer this) = " (probability(*p_yes)) }
1057        },
1058        Ending::NoQuestion => html! {
1059            p .answer.quiet { "Jev wasn't asked anything." }
1060            p .why { "The LLM didn't write a question Jev can take. Its reply is below." }
1061        },
1062        Ending::Failed { error } => html! {
1063            p .answer.quiet { "Jev didn't answer." }
1064            p .why { (error) }
1065        },
1066        Ending::Offline => html! {
1067            p .answer.quiet { "Jev is offline." }
1068            p .why { "This server has no TypeSafe API key, so nothing was sent." }
1069        },
1070        Ending::NotKept { id } => html! {
1071            p .answer.quiet { "Not asked yet." }
1072            p .why {
1073                "This version leads to a question Jev was never asked. Looking back costs nothing, so it was not sent. "
1074                (again(id, "↻ Ask it"))
1075            }
1076        },
1077        Ending::Spent { what } => html! {
1078            p .answer.quiet { "Out of budget for today." }
1079            p .why { "Everyone shares one daily budget for " (what) ", and it is used up. It resets at 00:00 UTC." }
1080        },
1081        Ending::Empty => html! { p .answer.quiet { "Ask me something." } },
1082    }
1083}

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

1086fn llm_card(made: &LlmCall) -> Markup {
1087    html! {
1088        details .win {
1089            summary .bar-title {
1090                span .fold {} (icon(&icons::LLM, "")) "llm · " (made.model)
1091                span .meta {
1092                    @match &made.outcome {
1093                        None => "writing Jev's question",
1094                        Some(LlmOutcome { sent: Sent::Before { at_ms }, .. }) => (kept(*at_ms)),
1095                        Some(outcome) => (format!("{} ms · {:.1} neurons", outcome.took.as_millis(), outcome.neurons)),
1096                    }
1097                }
1098            }
1099            div .body {
1100                (versions(made.kept.as_ref()))
1101                div .wire {
1102                    span .label-s { "request" }
1103                    (json(&made.request))
1104                }
1105                @if let Some(outcome) = &made.outcome {
1106                    div .wire {
1107                        span .label-s { "response" }
1108                        (json(&outcome.body))
1109                        @if outcome.dropped > 0 {
1110                            p .note { (outcome.dropped) " more tool calls were over the limit and were not run." }
1111                        }
1112                    }
1113                }
1114            }
1115        }
1116    }
1117}

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.

1121fn tool_card(tool: &Tool, sent: bool) -> Markup {
1122    html! {
1123        details .win open[tool.problem().is_some()] {
1124            summary .bar-title {
1125                span .fold {} (icon(&icons::TOOL, "")) "tool call · " (tool.call.name)
1126                span .meta {
1127                    @if tool.problem().is_some() { "not sent" } @else if sent { "sent to Jev as " (tool.id) } @else { (tool.id) }
1128                }
1129            }
1130            div .body {
1131                div .wire {
1132                    span .label-s { "the LLM's tool call" }
1133                    pre { (tool.call.name) }
1134                    (json(&tool.call.arguments))
1135                    @if let Some(problem) = tool.problem() {
1136                        p .note { "Not sent to Jev: " (problem) }
1137                    }
1138                }
1139            }
1140        }
1141    }
1142}

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.

1147fn again(id: &str, label: &str) -> Markup {
1148    html! {
1149        button .again type="button" title="Send this request again and keep the answer as its newest"
1150            "data-on:click"={ "$pins = ($pins || '') + ';" (id) ":fresh'; @post('/ask')" } { (label) }
1151    }
1152}

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

1155fn versions(kept: Option<&Versions>) -> Markup {
1156    let Some(kept) = kept else { return html! {} };
1157    let step = |to: u32, glyph: &str, label: &str| {
1158        html! {
1159            button .step type="button" title=(label)
1160                "data-on:click"={ "$pins = ($pins || '') + ';" (kept.id) ":" (to) "'; @post('/ask')" } { (glyph) }
1161        }
1162    };
1163    html! {
1164        div .versions {
1165            @if kept.version > 1 { (step(kept.version - 1, "◀", "The answer before")) }
1166            span .label-s { "answer " (kept.version) " of " (kept.versions) }
1167            @if kept.version < kept.versions { (step(kept.version + 1, "▶", "The answer after")) }
1168            (again(&kept.id, "↻ Ask again"))
1169        }
1170    }
1171}
1173fn jev_card(made: &JevCall) -> Markup {
1174    let questions = if made.parts.len() == 1 { "1 question".to_owned() } else { format!("{} questions", made.parts.len()) };
1175    html! {
1176        details .win open {
1177            summary .bar-title {
1178                span .fold {} (icon(&icons::TOOL, "")) "jev · " (made.id) " · " (questions)
1179                span .meta { (meta(made.outcome.as_ref())) }
1180            }
1181            div .body {
1182                (versions(made.kept.as_ref()))
1183                div .wire {
1184                    span .label-s { "request" }
1185                    pre {
1186                        "POST " (jev_protocol::ENDPOINT) "\n"
1187                        "authorization: Bearer [the server's key]\n"
1188                        "content-type: application/json"
1189                    }
1190                    (json(&made.request))
1191                }
1192                @match &made.outcome {
1193                    Some(Outcome::Answered { body, .. }) => div .wire {
1194                        span .label-s { "response" }
1195                        (json(body))
1196                    },
1197                    Some(Outcome::Failed { error, .. }) => div .wire {
1198                        span .label-s { "no response" }
1199                        pre { (error) }
1200                    },
1201                    None => {},
1202                }
1203            }
1204        }
1205    }
1206}

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.

1211fn json(text: &str) -> Markup {
1212    match serde_json::from_str::<Value>(text) {
1213        Ok(value) => html! { pre .json { (node(&value, 0)) } },
1214        Err(_) => html! { pre { (text) } },
1215    }
1216}
1218fn node(value: &Value, depth: usize) -> Markup {
1219    let indent = "  ".repeat(depth + 1);
1220    let close = "  ".repeat(depth);
1221    match value {
1222        Value::Object(map) if !map.is_empty() => html! {
1223            "{\n"
1224            @for (i, (key, child)) in map.iter().enumerate() {
1225                (indent) span .k { (quoted(key)) } ": " (node(child, depth + 1))
1226                @if i + 1 < map.len() { "," } "\n"
1227            }
1228            (close) "}"
1229        },
1230        Value::Array(items) if !items.is_empty() => html! {
1231            "[\n"
1232            @for (i, child) in items.iter().enumerate() {
1233                (indent) (node(child, depth + 1))
1234                @if i + 1 < items.len() { "," } "\n"
1235            }
1236            (close) "]"
1237        },
1238        Value::String(text) => html! { span .s { (quoted(text)) } },
1239        // Numbers, booleans, null, and the empty object and array.
1240        other => html! { span .n { (other.to_string()) } },
1241    }
1242}

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

1245fn quoted(text: &str) -> String {
1246    Value::String(text.to_owned()).to_string()
1247}

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

1251fn kept(at_ms: f64) -> String {
1252    format!("not sent · answered {}", when(at_ms))
1253}
1255fn meta(outcome: Option<&Outcome>) -> String {
1256    match outcome {
1257        None => "waiting for Jev".to_owned(),
1258        Some(Outcome::Answered { sent: Sent::Before { at_ms }, request_id, .. }) => {
1259            let mut parts = vec![kept(*at_ms)];
1260            parts.extend(request_id.clone());
1261            parts.join(" · ")
1262        }
1263        Some(Outcome::Answered { request_id, attempts, usage, took, .. }) => {
1264            let mut parts = vec![
1265                format!("{} ms", took.as_millis()),
1266                if *attempts == 1 { "1 attempt".to_owned() } else { format!("{attempts} attempts") },
1267                format!("{} tokens in", usage.input_tokens),
1268                format!("${:.6}", usage.dollars()),
1269            ];
1270            parts.extend(request_id.clone());
1271            parts.join(" · ")
1272        }
1273        Some(Outcome::Failed { request_id, took, .. }) => {
1274            let mut parts = vec!["failed".to_owned(), format!("{} ms", took.as_millis())];
1275            parts.extend(request_id.clone());
1276            parts.join(" · ")
1277        }
1278    }
1279}
1280
1281fn share(input: &str) -> Markup {
1282    let query: String = url_encode(input);
1283    html! {
1284        div .share {
1285            span { "Send it to them:" }
1286            a #link href={ "/?q=" (query) } { "/?q=" (query) }
1287            button .ghost type="button" data-copy="link" { "Copy link" }
1288        }
1289    }
1290}

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

1294pub fn url_encoded(text: &str) -> String {
1295    url_encode(text)
1296}
1298fn url_encode(text: &str) -> String {
1299    let mut out = String::with_capacity(text.len());
1300    for byte in text.bytes() {
1301        match byte {
1302            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(byte as char),
1303            b' ' => out.push('+'),
1304            _ => out.push_str(&format!("%{byte:02X}")),
1305        }
1306    }
1307    out
1308}
1309
1310fn probability(p: f64) -> String {
1311    format!("{p:.2}")
1312}
1313
1314#[cfg(test)]
1315mod tests;