The page and the transcript, as HTML. Pure: a View in, markup out.
7pub mod code; 8pub mod releases; 9pub mod repos; 10pub mod toast; 11 12use std::time::Duration; 13 14use archive::{Answer, Detail, Entry, Home, Stats, when}; 15use ask::{Judged, Outcome, Prepared, Sent}; 16use jev_protocol::{ChoiceAnswer, ScoreAnswer}; 17use llm::{Draft, ToolCall}; 18use maud::{DOCTYPE, Markup, PreEscaped, html}; 19use rules::{Fact, Kind, Known, Network}; 20use serde_json::Value; 21 22const CSS: &str = include_str!("page.css"); 23const JS: &str = include_str!("page.js");
The nearest open faces to typesafe.ai's own: Inter Tight for its grotesk, VT323 for its pixel terminal face, and JetBrains Mono, which it also uses.
26const FONTS: &str = "https://fonts.googleapis.com/css2?family=Inter+Tight:wght@500&family=JetBrains+Mono:wght@400;500&family=VT323&display=swap";
Pixel icons for the window title bars, 12 by 12, # for ink.
29mod icons { 30 pub const ASK: [&str; 12] = [ 31 "............", "..#####.....", ".#.....#....", ".#.....#....", ".#.....#....", ".#.....#....", 32 "..#####.....", ".......##...", "........##..", ".........##.", "..........#.", "............", 33 ]; 34 pub const JEV: [&str; 12] = [ 35 ".....##.....", "....#.##....", "...#.#.##...", "..#.#.#.##..", ".#.#.#.#.##.", "#.#.#.#.####", 36 "####.#.#.#.#", ".##.#.#.#.#.", "..##.#.#.#..", "...##.#.#...", "....##.#....", ".....##.....", 37 ]; 38 pub const LLM: [&str; 12] = [ 39 "............", ".#########..", ".#.......#..", ".#.##.##.#..", ".#.......#..", ".#.#####.#..", 40 ".#.......#..", ".#########..", "...##.......", "..##........", ".#..........", "............", 41 ]; 42 pub const TOOL: [&str; 12] = [ 43 "............", ".......###..", "......##....", "......##..#.", "......#####.", ".....####...", 44 "....###.....", "...###......", "..###.......", ".###........", ".##.........", "............", 45 ]; 46}
A pixel icon as inline SVG: one square per #.
49fn icon(rows: &[&str; 12], class: &str) -> Markup { 50 html! { 51 svg class={ "icon " (class) } viewBox="0 0 12 12" aria-hidden="true" { 52 @for (y, row) in rows.iter().enumerate() { 53 @for (x, cell) in row.bytes().enumerate() { 54 @if cell == b'#' { rect x=(x) y=(y) width="1" height="1" fill="currentColor" {} } 55 } 56 } 57 } 58 } 59}
One request to Jev, before and after its response. It may ask several questions.
63pub struct JevCall {
What the request is for: facts or answers.
65 pub id: &'static str,
Each question's id and Jev type, in the order asked.
67 pub parts: Vec<(String, &'static str)>,
The request body as sent.
69 pub request: String,
None while Jev is still answering.
71 pub outcome: Option<Outcome>,
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.
110 pub request: String,
None while the LLM is still writing.
112 pub outcome: Option<LlmOutcome>,
117pub struct LlmOutcome {
The reply as it came, or the error in its place.
119 pub body: String,
Tool calls the LLM made past the cap, which were not run.
124 pub dropped: usize,
Whether this ask sent the request, or was given the reply kept from the first time.
One tool call the LLM made.
131pub struct Tool {
139impl 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.
155pub enum Ending {
Nothing was typed; nothing was sent.
157 Empty,
The gate said this is not something Jev can judge.
159 NotAQuestion { p_yes: f64 },
Jev answered: the input itself, read one way or two, or what the LLM wrote. Which, is in the rules' notes.
162 Answered,
This visitor has asked too much in the last minute. Nothing was sent.
164 Slow,
The LLM replied, and nothing in the reply was a question Jev answered.
166 NoQuestion,
A call failed, or could not be built.
168 Failed { error: String },
The Worker has no API key.
170 Offline,
Today's shared budget for what is used up.
172 Spent { what: &'static str },
The page is looking at an old version of a call, and the call after it was never sent: nothing is sent while looking, so it waits for ↻.
One answer in the transcript.
179struct 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.
184 read_as: Option<&'static str>,
189impl Shown<'_> {
The answer as it is kept for the feed and for link previews.
191 fn told(&self) -> Answer { 192 let detail = match self.judged { 193 Judged::Noul(p) if self.chance => Detail::Chance { p: *p }, 194 Judged::Noul(p_yes) => Detail::Noul { p_yes: *p_yes }, 195 Judged::Choice(answer) => { 196 let mut options = answer.probabilities.clone(); 197 options.sort_by(|a, b| b.1.total_cmp(&a.1)); 198 Detail::Choice { confidence: answer.confidence, options } 199 } 200 Judged::Score(answer) => Detail::Score { 201 score: answer.score, 202 confidence: answer.confidence, 203 levels: answer.legend.iter().cloned().zip(answer.probabilities.iter().copied()).collect(), 204 }, 205 }; 206 Answer { headline: self.headline(), detail } 207 }
The answer in a few words: what the transcript prints large.
What is known about the input so far: what the rules run on.
222 pub known: Known,
The rules that decided a step, in order (indices into the network's terminals).
225 pub fired: Vec<usize>,
The request for the facts the rules wanted.
The request that asks Jev every question the LLM wrote.
231 pub judging: Option<JevCall>,
None while calls are still being made.
233 pub ending: Option<Ending>,
The card of the LLM suggestion's site, read before the ending is
shown (meatproxy::head); nothing if it could not be.
236 pub preview: Option<meatproxy::Head>,
$pins once this view is shown: every call shown at a version other
than its newest, so the page's next ↻ or ◀ ▶ keeps them as they are.
259 pub fn pinned(&self) -> String { 260 let kept = [self.facts.as_ref().and_then(|call| call.kept.as_ref()), self.llm.as_ref().and_then(|call| call.kept.as_ref()), self.judging.as_ref().and_then(|call| call.kept.as_ref())]; 261 let pins: Vec<String> = kept.into_iter().flatten().filter(|kept| kept.version < kept.versions).map(|kept| format!("{}:{}", kept.id, kept.version)).collect(); 262 pins.join(";") 263 }
How many of this view's calls were sent for it, and how many were answered from what was kept.
267 pub fn calls(&self) -> (u32, u32) { 268 let jev = [&self.facts, &self.judging].into_iter().flatten().filter_map(|call| match &call.outcome { 269 Some(Outcome::Answered { sent, .. }) => Some(*sent), 270 _ => None, 271 }); 272 let llm = self.llm.as_ref().and_then(|call| call.outcome.as_ref()).map(|outcome| outcome.sent); 273 let (mut sent, mut kept) = (0, 0); 274 for call in jev.chain(llm) { 275 match call { 276 Sent::Now => sent += 1, 277 Sent::Before { .. } => kept += 1, 278 } 279 } 280 (sent, kept) 281 }
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.
301 fn shown(&self, fact: Fact) -> Option<String> { 302 match fact { 303 Fact::Drafted => Some(self.tools.iter().filter(|tool| tool.draft().is_some()).count().to_string()), 304 Fact::Judged => Some(self.tools.iter().filter(|tool| self.judged(tool).is_some()).count().to_string()), 305 Fact::Reads(kind) => match self.fact(fact)? { 306 Judged::Choice(answer) => ask::kind_probability(answer, kind).map(probability), 307 _ => None, 308 }, 309 _ => match self.fact(fact)? { 310 Judged::Noul(p_yes) => Some(probability(*p_yes)), 311 Judged::Score(answer) => Some(format!("{:.1}", answer.score)), 312 Judged::Choice(_) => None, 313 }, 314 } 315 }
Whether Jev answered any question the LLM wrote.
The answers the rules say to show, in rule order: the input itself as Jev answered it with the facts (read as a yes-or-no, as a how-much, or both when Jev was split), then what the LLM wrote.
325 fn results(&self) -> Vec<Shown<'_>> { 326 let mut results = Vec::new(); 327 for fact in Network::lmjtfy().shows(&self.known) { 328 if fact == Fact::Judged { 329 results.extend(self.tools.iter().filter_map(|tool| { 330 let question = tool.call.draft.as_ref().ok()?.instructions(); 331 Some(Shown { question, judged: self.judged(tool)?, read_as: None, chance: false }) 332 })); 333 } else if let Some(judged) = self.fact(fact) { 334 let read_as = match fact { 335 Fact::Yes => "read as a yes or no", 336 Fact::Chance => "read as a how likely", 337 _ => "read as a how much", 338 }; 339 results.push(Shown { question: &self.input, judged, read_as: Some(read_as), chance: fact == Fact::Chance }); 340 } 341 } 342 // The reading is worth saying only when there is another beside it. 343 if results.len() == 1 { 344 results[0].read_as = None; 345 } 346 results 347 }
Whether there is any answer to show.
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.
383pub fn budget(shared: Option<Shared>) -> Markup { 384 html! { 385 div #budget .win { 386 div .bar-title { (icon(&icons::JEV, "")) "Shared budget" span .meta { "resets 00:00 UTC" } } 387 div .body { 388 @if let Some(shared) = shared { 389 @let left = (shared.neurons_per_day - shared.status.neurons).max(0.0); 390 @let jev_left = (shared.jev_dollars_per_day - shared.status.jev_dollars).max(0.0); 391 div .bars { 392 div .bar.won { 393 span .label { "LLM · free neurons left today" } 394 span .pct { (grouped(left)) " of " (grouped(shared.neurons_per_day)) } 395 span .track { span .thumb style=(at(left / shared.neurons_per_day)) {} } 396 } 397 div .bar.won { 398 span .label { "Jev · dollars left today" } 399 span .pct { (format!("${jev_left:.4} of ${:.2}", shared.jev_dollars_per_day)) } 400 span .track { span .thumb style=(at(jev_left / shared.jev_dollars_per_day)) {} } 401 } 402 } 403 p .fine { 404 "Everyone who opens this page spends from the same two pots. When one is empty, " 405 "the site stops answering until it resets. " 406 @match shared.status.counted { 407 budget::Counted::Account => "The neuron figure is Cloudflare's own count for the whole account.", 408 budget::Counted::Own => "The neuron figure is this site's own count; Cloudflare's could not be read.", 409 } 410 } 411 } @else { 412 p .fine { "The shared budget could not be read just now." } 413 } 414 } 415 } 416 } 417}
A line of a feed. A line of "asked lately" has an id from its question, so the line a question already has is the one a live update moves to the top; "most asked" repeats some of those questions, and has none.
422fn entry(entry: &Entry, id: Option<String>) -> Markup { 423 html! { 424 li .entry id=[id] { 425 a href={ "/?q=" (url_encode(&entry.input)) } title=(when(entry.asked_ms)) { (entry.input) } 426 span .said-s { 427 (entry.headlines()) 428 @if entry.times > 1 { span .times { " ×" (entry.times) } } 429 } 430 } 431 } 432}
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.
449fn more(shown: &[Entry]) -> Markup { 450 let after = (shown.len() == archive::FEED as usize).then(|| shown.last()).flatten(); 451 html! { 452 @if let Some(last) = after { 453 li #more "data-on-intersect__once"={ 454 "@get('/feed?ms=" (last.asked_ms) "&q=" (url_encode(&last.input)) "')" 455 } { span .meta { "more…" } } 456 } @else { 457 li #more hidden {} 458 } 459 } 460}
The page of the feed after a scroll: its lines, which go before the end, and the new end.
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.
491pub fn feed(home: Option<&Home>) -> Markup { 492 let lately = home.map(|home| home.lately.as_slice()).unwrap_or_default(); 493 let most_asked = home.map(|home| home.most.as_slice()).unwrap_or_default(); 494 html! { 495 div #feed .win { 496 div .bar-title { (icon(&icons::ASK, "")) "Asked lately" span .meta { "by nobody in particular" } } 497 div .body { 498 ol #lately .feed { @for asked in lately { (lately_entry(asked)) } (more(lately)) } 499 (most(most_asked)) 500 } 501 } 502 } 503}
The archive in numbers: how much Jev settled alone, and how many requests were never sent because their response was kept.
511pub fn tally(stats: Option<&Stats>) -> Markup { 512 let stats = stats.filter(|stats| stats.questions > 0); 513 html! { 514 div #tally .win[stats.is_some()] { 515 @if let Some(stats) = stats { 516 div .bar-title { (icon(&icons::JEV, "")) "So far" } 517 div .body { 518 ul .tally { 519 li { 520 b { (plural(stats.questions, "question", "questions")) } 521 " answered, asked " (plural(stats.asks, "time", "times")) "." 522 } 523 @if let Some(percent) = stats.no_llm_percent() { 524 li { b { (percent) "%" } " of them Jev answered alone, with no LLM." } 525 } 526 li { 527 b { (plural(stats.sent, "request", "requests")) } 528 " sent to Jev and the LLM. " 529 b { (grouped(f64::from(stats.kept))) } 530 " more were not sent, because the same request had been answered before." 531 } 532 @if stats.clones + stats.pulls > 0 { 533 li { 534 "The code was " a href=(CODE) { "cloned" } " " 535 b { (plural(stats.clones, "time", "times")) } " and pulled " 536 b { (plural(stats.pulls, "time", "times")) } "." 537 } 538 } 539 } 540 } 541 } 542 } 543 } 544}
A short mark of an answer, for the address of its picture: whoever shows previews keeps a picture by its address, so the address has to change when the answer does (a question asked for the first time, most of all).
What a link to the page unfurls as, in Discord and wherever else reads
Open Graph tags. The home page says what the site is. A ?q= link shows
the question, and Jev's answer when it has been asked before: a preview
is built from what is kept and never asks anything. origin is where
the site is, for the picture's address.
560fn preview(typed: &str, said: Option<&Entry>, declined: bool, origin: &str) -> Markup { 561 // Each answer with its numbers: the probability, the confidence, the 562 // likeliest options. That is half of what Jev says. 563 let answer = said.filter(|entry| !entry.answers.is_empty()).map(|entry| { 564 let lines: Vec<String> = entry.answers.iter().map(Answer::line).collect(); 565 lines.join(" / ") 566 }); 567 let (title, description, picture) = if typed.is_empty() { 568 ( 569 "Let Me Jev That For You".to_owned(), 570 "Ask Jev anything. It answers yes, no, which one or how much, with a probability, and shows its work." 571 .to_owned(), 572 format!("{origin}/card.png"), 573 ) 574 } else { 575 // A question Jev could not take says so, and nothing of Jev's: it 576 // points at the LLMs the page does (`meatproxy`), and its picture 577 // is the card for that (`card::Card::Declined`). 578 let declined = declined && answer.is_none(); 579 let description = match &answer { 580 Some(answer) => format!("Jev: {answer}"), 581 None if declined => "Please choose an LLM instead: Claude, ChatGPT or Gemini.".to_owned(), 582 None => "Let me Jev that for you.".to_owned(), 583 }; 584 let picture = format!( 585 "{origin}/card.png?q={}&a={}", 586 url_encode(typed), 587 mark(if declined { "declined" } else { answer.as_deref().unwrap_or_default() }) 588 ); 589 (format!("Q: {typed}"), description, picture) 590 }; 591 html! { 592 title { (title) } 593 meta name="description" content=(description); 594 meta property="og:type" content="website"; 595 meta property="og:site_name" content="LMJTFY · Let Me Jev That For You"; 596 meta property="og:title" content=(title); 597 meta property="og:description" content=(description); 598 meta property="og:image" content=(picture); 599 meta property="og:image:type" content="image/png"; 600 meta property="og:image:width" content=(card::WIDTH); 601 meta property="og:image:height" content=(card::HEIGHT); 602 // The picture is shown large, under the text. 603 meta name="twitter:card" content="summary_large_image"; 604 // The bar down the side of the embed: the page's pink. 605 meta name="theme-color" content="#f386a1"; 606 } 607}
The start of every page: what it unfurls as, the type and the look.
610fn head(preview: Markup) -> Markup { 611 html! { 612 head { 613 meta charset="utf-8"; 614 meta name="viewport" content="width=device-width, initial-scale=1"; 615 // The build this page came from, for `/live` to compare (page.js). 616 meta name="lmjtfy-build" content=(crate::BUILD); 617 (preview) 618 link rel="preconnect" href="https://fonts.googleapis.com"; 619 link rel="preconnect" href="https://fonts.gstatic.com" crossorigin; 620 link rel="stylesheet" href=(FONTS); 621 style { (PreEscaped(CSS)) } 622 } 623 } 624}
The bar at the top of every page. On the code host (host) the title
leads home to the other address, and there is no online count or toasts:
that host has no /live, and page.js, finding no #online, opens no
socket and sends no /seen.
630fn nav(host: Host) -> Markup { 631 html! { 632 nav { 633 a href=(host.home_link()) { "LMJTFY" } 634 @if host.is_live() { 635 // Filled in over `/live` (page.js): pages open now, and a toast 636 // for each question asked and each clone while this one is open. 637 span .online-wrap tabindex="0" { 638 span #online .online hidden {} 639 (places(&[])) 640 } 641 div #toasts aria-live="polite" {} 642 } 643 a .dark href=(CODE) { "Code" } 644 a .dark href="https://docs.typesafe.ai" { "What is Jev?" } 645 } 646 } 647}
Whether Jev was right, as the people who asked think: a vote each, one per
browser, taken back by pressing it again. rating is None when there
is nothing to vote on yet; can is false for a browser with no id.
652pub fn rating(rating: Option<&archive::Rating>, can: bool) -> Markup { 653 // The thumbs mean otherwise on a question Jev declined: up is "right to 654 // pass", down is "could have answered". 655 let declined = rating.is_some_and(|rating| rating.declined); 656 let button = |vote: archive::Vote, glyph: &str, n: u32, label: &str| { 657 let on = rating.and_then(|rating| rating.mine) == Some(vote); 658 let name = match vote { 659 archive::Vote::Up => "up", 660 archive::Vote::Down => "down", 661 }; 662 html! { 663 button .vote .on[on] type="button" disabled[!can] aria-pressed=(on) title=(label) 664 "data-on:click"={ "$vote = '" (name) "'; @post('/rate')" } { 665 span .glyph aria-hidden="true" { (glyph) } " " (n) 666 } 667 } 668 }; 669 html! { 670 div #rating .rating { 671 @if let Some(shown) = rating { 672 @if declined { 673 span .label-s { "Was Jev right to pass?" } 674 (button(archive::Vote::Up, "👍", shown.up, "Jev was right to pass")) 675 (button(archive::Vote::Down, "👎", shown.down, "Jev could have answered")) 676 } @else { 677 span .label-s { "Was Jev right?" } 678 (button(archive::Vote::Up, "👍", shown.up, "Jev was right")) 679 (button(archive::Vote::Down, "👎", shown.down, "Jev was wrong")) 680 } 681 @if !can { span .meta { "Voting needs this site's cookie: reload the page." } } 682 } 683 } 684 } 685}
The box that opens under the vote (/comment). It is on the page from the
first answer and opens by the open signal, which /rate sets while the
browser has a vote, so the transition is the page's own CSS and a pressed
vote costs no markup. It is never patched but for its two notes: the text
lives in the comment signal, so a patch cannot take it away, and a
failed save leaves it where it was. Closed, it is inert, so it is not
a tab stop either.
Under a question Jev declined (declined) it asks why Jev could have
answered, and how the site can be better.
697pub fn comment_box(declined: bool) -> Markup { 698 let placeholder = if declined { "Why could Jev have answered? Or how can we improve?" } else { "Let us know what we can improve" }; 699 html! { 700 div #comment .comment data-class:open="$open" data-attr:inert="!$open" { 701 div .comment-inner { 702 label .label-s for="comment-text" { "Comments?" } 703 textarea #comment-text .comment-text name="comment" rows="3" maxlength=(archive::MOST_COMMENT) 704 placeholder=(placeholder) data-bind:comment "data-on:input"="$saved = false" {} 705 div .comment-actions { 706 button .vote type="button" data-attr:disabled="$saved || $comment.trim() === ''" "data-on:click"="@post('/comment')" { "Save" } 707 span #comment-thanks .thanks data-class:shown="$saved" role="status" { "Thanks! Your comment was saved." } 708 (comment_error(None)) 709 } 710 } 711 } 712 } 713}
Why a comment was not saved, said where the Save button is. Fixed words: nothing the visitor typed is ever echoed into it.
717pub fn comment_error(unsaid: Option<archive::Unsaid>) -> Markup { 718 use archive::Unsaid; 719 html! { 720 span #comment-error .meta role="alert" { 721 @match unsaid { 722 None => {} 723 Some(Unsaid::Empty) => { "Write something first." } 724 Some(Unsaid::TooLong) => { "That is over " (archive::MOST_COMMENT) " characters: shorten it and Save again." } 725 Some(Unsaid::NoVote) => { "Vote first, then comment." } 726 Some(Unsaid::Limit) => { "Slow down a little: try again in a minute." } 727 Some(Unsaid::Failed) => { "That could not be saved. Your text is still here: try again." } 728 } 729 } 730 } 731}
Where the open pages are, for the drop-down under "N online": a flag and
a place a line, most first. Replaced whole over /live when a page comes
or goes.
736pub fn places(open: &[(archive::Place, u32)]) -> Markup { 737 html! { 738 ul #places .places { 739 @for (place, n) in open { 740 li { 741 span .flag aria-hidden="true" { (place.flag().unwrap_or_else(|| "·".to_owned())) } 742 span .where { (place.label()) } 743 @if *n > 1 { span .times { "×" (n) } } 744 } 745 } 746 } 747 } 748}
Where the code is cloned from, and its landing page: the same address.
751const CODE: &str = "/lmjtfy.git";
The foot of the home page and the code page: what the site is, and on the home page, how to get its code (the code page has it at the top).
755fn footer(clone: Option<&str>) -> Markup { 756 html! { 757 footer { 758 div .wrap { 759 div .about { 760 (icon(&icons::JEV, "big")) 761 span { 762 "Jev answers typed questions with calibrated probabilities, not text. " 763 "Rules decide what to ask it, and an LLM writes the options when a question needs them. " 764 a href="/rules" { "See the rules." } " " 765 "Not affiliated with TypeSafe AI." 766 } 767 } 768 @if let Some(origin) = clone { 769 (clone_line(&crate::clone::command(origin), html! { a href={ (origin) (CODE) } { "Get the code:" } })) 770 } 771 } 772 } 773 } 774}
A clone command with a copy button.
The same, for a page with several: id is what the copy button copies
(page.js's data-copy), so it is one per command on the page.
The rules with facts set by hand (playground.rs): every test is a link.
794pub fn playground( 795 network: &Network, 796 known: &Known, 797 fired: &[usize], 798 says: &str, 799 link: impl Fn(diagram::Clicked) -> String, 800) -> Markup { 801 let title = "The rules · LMJTFY"; 802 let about = "The rules that decide what happens to a question, as a Rete network you can set the facts of."; 803 html! { 804 (DOCTYPE) 805 html lang="en" { 806 (head(html! { 807 title { (title) } 808 meta name="description" content=(about); 809 meta property="og:title" content=(title); 810 meta property="og:description" content=(about); 811 meta name="theme-color" content="#f386a1"; 812 })) 813 body { 814 div .sheet { 815 div .wrap { 816 (nav(Host::Home)) 817 div .stack { 818 div .win { 819 div .bar-title { 820 (icon(&icons::TOOL, "")) "rules · rete" 821 span .meta { 822 @if fired.is_empty() { "nothing decided yet" } @else { (diagram::sequence(network, fired)) } 823 } 824 } 825 div .body { 826 (diagram::rete(network, known, fired, |_| None, |clicked| Some(link(clicked)))) 827 p .says { (says) } 828 p .fine { 829 "Click a test to say its fact is so, or a fact to step through what it can be. " 830 "Nothing is asked of Jev or the LLM here: the facts are whatever you set. " 831 a href="/rules" { "Start over." } 832 } 833 p .fine { 834 "On a real question, Jev gives every fact in the first frame in one " 835 "request, and the rules take it from there. Filled is what a rule that " 836 "holds stands on, outlined is known and not needed, dashed failed." 837 } 838 } 839 } 840 } 841 } 842 } 843 // The online count and the toasts, as on every page. 844 script { (PreEscaped(JS)) } 845 } 846 } 847 } 848}
said is the kept answer to typed, if there is one, for the preview.
origin is where this page is, for the preview's picture; code is where
the code is cloned from (host::Host::code_origin), for the command at the foot.
854pub fn page(typed: &str, said: Option<&Entry>, declined: bool, origin: &str, code: &str, shared: Option<Shared>, home: Option<&Home>) -> Markup { 855 html! { 856 (DOCTYPE) 857 html lang="en" { 858 (head(html! { 859 (preview(typed, said, declined, origin)) 860 script type="module" src="/datastar.js" {} 861 })) 862 body data-signals="{q: '', pins: '', comment: '', open: false, saved: false}" { 863 div .sheet { 864 div .wrap { 865 (nav(Host::Home)) 866 div .headline { 867 p .eyebrow { "Let me ........................ Jev that for you" } 868 h1 { "Let Me Jev That For You" } 869 } 870 div .stack { 871 div .win { 872 div .bar-title { (icon(&icons::ASK, "")) "Ask" } 873 div .body { 874 form #ask data-type=(typed) data-on:submit__prevent="@post('/ask')" data-indicator:asking { 875 // Jev answers in about a fifth of a second, so the 876 // gate runs on every pause in the typing. 877 // A textarea so a long question wraps and the box grows 878 // with it (page.css, page.js); Enter still asks, and a 879 // question is one line, so no newline gets in (page.js). 880 textarea #q name="q" rows="1" data-bind:q "data-on:input__debounce.300ms"="@post('/gate')" 881 autocomplete="off" required maxlength=(ask::MAX_INPUT_CHARS) 882 placeholder="Ask Jev anything" aria-label="Your question" {} 883 button type="submit" data-attr:disabled="$asking" { "Ask Jev" } 884 } 885 (suggested(&[])) 886 (live(None)) 887 } 888 } 889 (budget(shared)) 890 (feed(home)) 891 (tally(home.map(|home| &home.stats))) 892 } 893 } 894 } 895 (resting()) 896 (footer(Some(code))) 897 script { (PreEscaped(JS)) } 898 } 899 } 900 } 901}
What the facts say about the text so far, for the line under the box.
904pub struct Glance {
The line under the box: what Jev makes of the text so far.
922pub fn live(glance: Option<Glance>) -> Markup { 923 html! { 924 p #live .live aria-live="polite" { 925 @if let Some(glance) = glance { 926 @if glance.unfinished { 927 // Half a question is not judged yet: what Jev would say 928 // of it is not what it will say of the whole. 929 span .dot {} "Jev reads this as unfinished · keep typing" 930 } @else if ask::is_yes(glance.answerable) { 931 span .dot.yes {} "Jev can judge this" 932 @if let Some(kind) = glance.kind { " · " (kind_words(kind)) } 933 } @else { 934 span .dot {} "Jev can't judge this yet" 935 } 936 @if !glance.unfinished { span .meta { " · " (probability(glance.answerable)) } } 937 // The way forward, so a low score mid-sentence does not end the 938 // typing: asking shows where an LLM can answer (see meatproxy.rs). 939 // Short enough for one line beside the pixel font at desktop width. 940 @if !glance.unfinished && !ask::is_yes(glance.answerable) { span .meta { " · press Ask for an LLM that can" } } 941 } 942 } 943 } 944}
Under the box as the visitor types: questions already on the feed that what is typed could be the start of, each with Jev's answer. Following one asks it again. Empty when there are none.
The transcript before anything is asked.
The transcript as it stands. Sent whole each time; Datastar morphs it in by id.
963pub fn transcript(view: &View) -> Markup { 964 let results = view.results(); 965 let said = view.ending.is_some() || !results.is_empty(); 966 html! { 967 div #transcript { 968 div .sheet { 969 div .wrap { 970 p .step { (step(view)) } 971 div .win.white { 972 div .bar-title { (icon(&icons::JEV, "")) "Jev" } 973 div .body { 974 div .row { span .who { "Q:" } span .q { (view.input) } } 975 @if said { 976 div .row { span .who { "Jev:" } div .said { 977 @for shown in &results { (result(shown)) } 978 @if let Some(ending) = &view.ending { (ended(ending, &view.input, view.preview.as_ref())) } 979 } } 980 } 981 // Filled in once the answer, or the declining, is kept 982 // (`/rate`); the thumbs mean something else on the latter. 983 @if matches!(view.ending, Some(Ending::Answered | Ending::NotAQuestion { .. } | Ending::NoQuestion)) { 984 div #rating .rating {} 985 (comment_box(!matches!(view.ending, Some(Ending::Answered)))) 986 } 987 @if matches!(view.ending, Some(Ending::Answered | Ending::NotAQuestion { .. })) { 988 (share(&view.input)) 989 } 990 } 991 } 992 } 993 } 994 @if view.facts.is_some() { 995 div .band { 996 div .wrap.stack { 997 (rules_card(view)) 998 @if let Some(facts) = &view.facts { (jev_card(facts)) } 999 @if let Some(llm) = &view.llm { (llm_card(llm)) } 1000 @for tool in &view.tools { (tool_card(tool, view.judging.is_some())) } 1001 @if let Some(judging) = &view.judging { (jev_card(judging)) } 1002 } 1003 } 1004 } 1005 } 1006 } 1007}
1009fn step(view: &View) -> &'static str { 1010 match &view.ending { 1011 Some(Ending::Empty) => "Type a question first", 1012 Some(Ending::Answered | Ending::NotAQuestion { .. }) => "Was that so hard?", 1013 Some(_) => "Nothing came back", 1014 None if view.judging.is_some() => "Step 3: asking Jev", 1015 None if view.llm.is_some() => "Step 2: an LLM is writing Jev's question", 1016 None if view.facts.is_some() => "Step 1: asking Jev what kind of question that is", 1017 None => "Getting Jev", 1018 } 1019}
The rules as a Rete network, with what is known about this query marked
on it (diagram.rs says what each mark means).
1023fn rules_card(view: &View) -> Markup { 1024 let network = Network::lmjtfy(); 1025 html! { 1026 details .win open { 1027 summary .bar-title { 1028 span .fold {} (icon(&icons::TOOL, "")) "rules · rete" 1029 span .meta { 1030 // No rule decides a step when Jev's first answers settle 1031 // it: then the rules that hold are the story. 1032 @let holding = network.holding(&view.known); 1033 @if !view.fired.is_empty() { 1034 (diagram::sequence(&network, &view.fired)) 1035 } @else if holding.is_empty() { 1036 "waiting on facts" 1037 } @else { 1038 (holding.join(" · ")) 1039 } 1040 } 1041 } 1042 div .body { 1043 (diagram::rete(&network, &view.known, &view.fired, |fact| view.shown(fact), |_| None)) 1044 p .fine { 1045 "Facts on the left, with what Jev said; rules on the right, numbered in the order they " 1046 "decided; the tests that join them between. Filled is what a rule that fired stood on, " 1047 "outlined was learned and not needed, dashed failed. A line from a rule back to a fact " 1048 "is a call that teaches it. " 1049 a href="/rules" { "Try the rules yourself." } 1050 } 1051 } 1052 } 1053 } 1054}
What Jev said to one question.
1057fn result(shown: &Shown<'_>) -> Markup { 1058 html! { 1059 div .result { 1060 p .asked { 1061 (shown.question) 1062 @if let Some(read_as) = shown.read_as { span .read-as { " · " (read_as) } } 1063 } 1064 @match shown.judged { 1065 // The probability is the answer; the note says it is a Noul, 1066 // and how to read the number. 1067 Judged::Noul(_) if shown.chance => { 1068 p .answer { (shown.headline()) } 1069 p .verdict { (shown.told().note().unwrap_or_default()) } 1070 }, 1071 Judged::Noul(p_yes) => { 1072 p .answer { (headline(shown.judged)) } 1073 p .verdict { "Noul · p(yes) = " (probability(*p_yes)) } 1074 }, 1075 Judged::Choice(answer) => (choice(answer)), 1076 Judged::Score(answer) => (score(answer)), 1077 } 1078 } 1079 } 1080}
A Noul as a word: flat when Jev is sure, a question when it is not.
The level a Score fell nearest, and the highest level there is. The
score is Jev's answer: the probability-weighted level, which "can land
between levels" (TypeSafe's docs), so this is the level it rounds to.
The level with the most probability (the lowest of equals). It can differ from the nearest: a spread-out answer has a mean on a level that is not the likeliest, which is what a low confidence says.
An answer in a few words: what the transcript prints large, and what the feed prints beside the question.
1109fn headline(judged: &Judged) -> String { 1110 match judged { 1111 Judged::Noul(p_yes) => yes_or_no(*p_yes).to_owned(), 1112 Judged::Choice(answer) => format!("{}.", answer.choice), 1113 Judged::Score(answer) => { 1114 let level = answer.legend.get(nearest(answer).0).map(String::as_str).unwrap_or_default(); 1115 format!("{level}{}", if ask::is_confident(answer.confidence) { "." } else { "?" }) 1116 } 1117 } 1118}
Where the square sits on its rule, from a probability.
Every option with its probability, most likely first.
1130fn choice(answer: &ChoiceAnswer) -> Markup { 1131 let mut ranked: Vec<&(String, f64)> = answer.probabilities.iter().collect(); 1132 ranked.sort_by(|a, b| b.1.total_cmp(&a.1)); 1133 html! { 1134 p .answer.pick { (headline(&Judged::Choice(answer.clone()))) } 1135 div .bars { 1136 @for (label, p) in ranked { 1137 div .bar.won[*label == answer.choice] { 1138 span .label { (label) } 1139 span .pct { (percent(*p)) } 1140 span .track { span .thumb style=(at(*p)) {} } 1141 } 1142 } 1143 } 1144 p .verdict { "Choice · confidence " (probability(answer.confidence)) } 1145 } 1146}
Every level with its probability, lowest first. The headline's level (the one nearest the score) is inked; the likeliest is tagged, and is the same level when Jev is sure. Under the bars: the score, which is the mean, and how sure Jev is.
1152fn score(answer: &ScoreAnswer) -> Markup { 1153 let (nearest, top) = nearest(answer); 1154 let likeliest = likeliest(answer); 1155 let sure = ask::is_confident(answer.confidence); 1156 html! { 1157 p .answer.pick { 1158 (headline(&Judged::Score(answer.clone()))) 1159 span .of { " mean " (format!("{:.1}", answer.score)) " / " (top) } 1160 } 1161 div .bars { 1162 @for (level, p) in answer.probabilities.iter().enumerate() { 1163 div .bar.won[level == nearest] { 1164 span .label { 1165 (level) " · " (answer.legend.get(level).map(String::as_str).unwrap_or_default()) 1166 @if level == likeliest { " " span .tag { "likeliest" } } 1167 } 1168 span .pct { (percent(*p)) } 1169 span .track { span .thumb style=(at(*p)) {} } 1170 } 1171 } 1172 } 1173 p .verdict { 1174 "Score · 0 to " (top) " · confidence " (probability(answer.confidence)) 1175 @if !sure { " · unsure: the levels are ambiguous" } 1176 } 1177 } 1178}
1180fn ended(ending: &Ending, input: &str, preview: Option<&meatproxy::Head>) -> Markup { 1181 match ending { 1182 Ending::Answered => html! {}, 1183 Ending::Slow => html! { 1184 p .answer.quiet { "Slow down." } 1185 p .why { "That is a lot of questions in one minute. Nothing was sent. Try again in a minute." } 1186 }, 1187 Ending::NotAQuestion { p_yes } => html! { 1188 p .answer { (yes_or_no(*p_yes)) } 1189 p .why { "That's not a question I can answer. Ask me yes or no, which one, or how much." } 1190 (meatproxy::suggestion(input, preview)) 1191 p .verdict { "p(I can answer this) = " (probability(*p_yes)) } 1192 }, 1193 Ending::NoQuestion => html! { 1194 p .answer.quiet { "Jev wasn't asked anything." } 1195 p .why { "The LLM didn't write a question Jev can take. Its reply is below." } 1196 (meatproxy::suggestion(input, preview)) 1197 }, 1198 Ending::Failed { error } => html! { 1199 p .answer.quiet { "Jev didn't answer." } 1200 p .why { (error) } 1201 }, 1202 Ending::Offline => html! { 1203 p .answer.quiet { "Jev is offline." } 1204 p .why { "This server has no TypeSafe API key, so nothing was sent." } 1205 }, 1206 Ending::NotKept { id } => html! { 1207 p .answer.quiet { "Not asked yet." } 1208 p .why { 1209 "This version leads to a question Jev was never asked. Looking back costs nothing, so it was not sent. " 1210 (again(id, "↻ Ask it")) 1211 } 1212 }, 1213 Ending::Spent { what } => html! { 1214 p .answer.quiet { "Out of budget for today." } 1215 p .why { "Everyone shares one daily budget for " (what) ", and it is used up. It resets at 00:00 UTC." } 1216 }, 1217 Ending::Empty => html! { p .answer.quiet { "Ask me something." } }, 1218 } 1219}
The LLM's call: what it was sent and what it wrote.
1222fn llm_card(made: &LlmCall) -> Markup { 1223 html! { 1224 details .win { 1225 summary .bar-title { 1226 span .fold {} (icon(&icons::LLM, "")) "llm · " (made.model) 1227 span .meta { 1228 @match &made.outcome { 1229 None => "writing Jev's question", 1230 Some(LlmOutcome { sent: Sent::Before { at_ms }, .. }) => (kept(*at_ms)), 1231 Some(outcome) => (format!("{} ms · {:.1} neurons", outcome.took.as_millis(), outcome.neurons)), 1232 } 1233 } 1234 } 1235 div .body { 1236 (versions(made.kept.as_ref())) 1237 div .wire { 1238 span .label-s { "request" } 1239 (json(&made.request)) 1240 } 1241 @if let Some(outcome) = &made.outcome { 1242 div .wire { 1243 span .label-s { "response" } 1244 (json(&outcome.body)) 1245 @if outcome.dropped > 0 { 1246 p .note { (outcome.dropped) " more tool calls were over the limit and were not run." } 1247 } 1248 } 1249 } 1250 } 1251 } 1252 } 1253}
One tool call the LLM made: what it wrote, and whether it went to Jev.
sent is whether a request to Jev was made at all.
1257fn tool_card(tool: &Tool, sent: bool) -> Markup { 1258 html! { 1259 details .win open[tool.problem().is_some()] { 1260 summary .bar-title { 1261 span .fold {} (icon(&icons::TOOL, "")) "tool call · " (tool.call.name) 1262 span .meta { 1263 @if tool.problem().is_some() { "not sent" } @else if sent { "sent to Jev as " (tool.id) } @else { (tool.id) } 1264 } 1265 } 1266 div .body { 1267 div .wire { 1268 span .label-s { "the LLM's tool call" } 1269 pre { (tool.call.name) } 1270 (json(&tool.call.arguments)) 1271 @if let Some(problem) = tool.problem() { 1272 p .note { "Not sent to Jev: " (problem) } 1273 } 1274 } 1275 } 1276 } 1277 } 1278}
A request to Jev: the body as sent and the response as it arrived.
↻ for the call id: send its request again, and work the rest out from
the answer.
◀ version n of m ▶ and ↻, for a call that has an answer.
1291fn versions(kept: Option<&Versions>) -> Markup { 1292 let Some(kept) = kept else { return html! {} }; 1293 let step = |to: u32, glyph: &str, label: &str| { 1294 html! { 1295 button .step type="button" title=(label) 1296 "data-on:click"={ "$pins = ($pins || '') + ';" (kept.id) ":" (to) "'; @post('/ask')" } { (glyph) } 1297 } 1298 }; 1299 html! { 1300 div .versions { 1301 @if kept.version > 1 { (step(kept.version - 1, "◀", "The answer before")) } 1302 span .label-s { "answer " (kept.version) " of " (kept.versions) } 1303 @if kept.version < kept.versions { (step(kept.version + 1, "▶", "The answer after")) } 1304 (again(&kept.id, "↻ Ask again")) 1305 } 1306 } 1307}
1309fn jev_card(made: &JevCall) -> Markup { 1310 let questions = if made.parts.len() == 1 { "1 question".to_owned() } else { format!("{} questions", made.parts.len()) }; 1311 html! { 1312 details .win open { 1313 summary .bar-title { 1314 span .fold {} (icon(&icons::TOOL, "")) "jev · " (made.id) " · " (questions) 1315 span .meta { (meta(made.outcome.as_ref())) } 1316 } 1317 div .body { 1318 (versions(made.kept.as_ref())) 1319 div .wire { 1320 span .label-s { "request" } 1321 pre { 1322 "POST " (jev_protocol::ENDPOINT) "\n" 1323 "authorization: Bearer [the server's key]\n" 1324 "content-type: application/json" 1325 } 1326 (json(&made.request)) 1327 } 1328 @match &made.outcome { 1329 Some(Outcome::Answered { body, .. }) => div .wire { 1330 span .label-s { "response" } 1331 (json(body)) 1332 }, 1333 Some(Outcome::Failed { error, .. }) => div .wire { 1334 span .label-s { "no response" } 1335 pre { (error) } 1336 }, 1337 None => {}, 1338 } 1339 } 1340 } 1341 } 1342}
A JSON body, indented and coloured for reading. Only whitespace is added: keys keep their order and every value is printed as it was sent. Text that is not JSON is shown as it is.
1354fn node(value: &Value, depth: usize) -> Markup { 1355 let indent = " ".repeat(depth + 1); 1356 let close = " ".repeat(depth); 1357 match value { 1358 Value::Object(map) if !map.is_empty() => html! { 1359 "{\n" 1360 @for (i, (key, child)) in map.iter().enumerate() { 1361 (indent) span .k { (quoted(key)) } ": " (node(child, depth + 1)) 1362 @if i + 1 < map.len() { "," } "\n" 1363 } 1364 (close) "}" 1365 }, 1366 Value::Array(items) if !items.is_empty() => html! { 1367 "[\n" 1368 @for (i, child) in items.iter().enumerate() { 1369 (indent) (node(child, depth + 1)) 1370 @if i + 1 < items.len() { "," } "\n" 1371 } 1372 (close) "]" 1373 }, 1374 Value::String(text) => html! { span .s { (quoted(text)) } }, 1375 // Numbers, booleans, null, and the empty object and array. 1376 other => html! { span .n { (other.to_string()) } }, 1377 } 1378}
A string as JSON writes it, quotes and escapes included.
A call's title bar when nothing was sent: the response is the one kept from the first time this exact request was made.
1391fn meta(outcome: Option<&Outcome>) -> String { 1392 match outcome { 1393 None => "waiting for Jev".to_owned(), 1394 Some(Outcome::Answered { sent: Sent::Before { at_ms }, request_id, .. }) => { 1395 let mut parts = vec![kept(*at_ms)]; 1396 parts.extend(request_id.clone()); 1397 parts.join(" · ") 1398 } 1399 Some(Outcome::Answered { request_id, attempts, usage, took, .. }) => { 1400 let mut parts = vec![ 1401 format!("{} ms", took.as_millis()), 1402 if *attempts == 1 { "1 attempt".to_owned() } else { format!("{attempts} attempts") }, 1403 format!("{} tokens in", usage.input_tokens), 1404 format!("${:.6}", usage.dollars()), 1405 ]; 1406 parts.extend(request_id.clone()); 1407 parts.join(" · ") 1408 } 1409 Some(Outcome::Failed { request_id, took, .. }) => { 1410 let mut parts = vec!["failed".to_owned(), format!("{} ms", took.as_millis())]; 1411 parts.extend(request_id.clone()); 1412 parts.join(" · ") 1413 } 1414 } 1415} 1416 1417fn share(input: &str) -> Markup { 1418 let query: String = url_encode(input); 1419 html! { 1420 div .share { 1421 span { "Send it to them:" } 1422 a #link href={ "/?q=" (query) } { "/?q=" (query) } 1423 button .ghost type="button" data-copy="link" { "Copy link" } 1424 } 1425 } 1426}
Percent-encodes everything but unreserved characters, for a query value.
text percent-encoded, for a URL or a header.
1434fn url_encode(text: &str) -> String { 1435 let mut out = String::with_capacity(text.len()); 1436 for byte in text.bytes() { 1437 match byte { 1438 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(byte as char), 1439 b' ' => out.push('+'), 1440 _ => out.push_str(&format!("%{byte:02X}")), 1441 } 1442 } 1443 out 1444} 1445 1446fn probability(p: f64) -> String { 1447 format!("{p:.2}") 1448} 1449 1450#[cfg(test)] 1451mod tests;