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>,
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).
The answer to the question called id, if Jev gave one.
The call to the LLM that writes Jev's questions.
The request body as sent.
106 pub request: String,
None while the LLM is still writing.
108 pub outcome: Option<LlmOutcome>,
113pub struct LlmOutcome {
The reply as it came, or the error in its place.
115 pub body: String,
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.
One tool call the LLM made.
127pub struct Tool {
135impl Tool {
The question to send to Jev, if the call is one.
Why this call was not sent to Jev, if it was not.
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 ↻.
One answer in the transcript.
175struct Shown<'a> {
The question it answers: the input, or what the LLM wrote.
How the input was read, when it was read more than one way.
180 read_as: Option<&'static str>,
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.
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.
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>,
$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 }
The kind Jev thinks the input most likely is.
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.
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.
What Jev said to each question it answered, as it is kept for the feed and for link previews.
What is used of the budgets every visitor shares, and their limits.
A whole number with thousands separators.
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}
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.
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.
A question just asked, for the top of every open page's feed.
The most asked questions, replaced whole when they change.
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}
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).
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.
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.
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.
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.
The level a Score fell nearest, and the highest level there is.
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.
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.
◀ 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.
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.
A call's title bar when nothing was sent: the response is the one kept from the first time this exact request was made.
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.
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;