From ffef841fd95c89a039a6940c18a8852ef1e6525e Mon Sep 17 00:00:00 2001 From: Lukas Scheucher Date: Sat, 6 Jun 2026 13:09:20 +0200 Subject: [PATCH] Arith: lead with the answer + "surprise me" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arithmetic page showed a flat ranked list; the actual analogy answer was buried at position 1. Now it reads like one. - Big animated "≈ {answer}" reveal (the top hit, shimmer, with its cosine), so "king − man + woman ≈ queen" lands immediately. Click it to chain it into the equation. Remaining hits become a quieter "others nearby" list (same similarity bars). - "✦ surprise me" shuffles through a curated set of analogies, filtered at runtime to ones whose terms are all in the corpus (never a dead shuffle). Independent of the word-dossier stack — touches only app/arith/page.tsx, mergeable on its own. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/app/arith/page.tsx | 155 +++++++++++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 36 deletions(-) diff --git a/web/app/arith/page.tsx b/web/app/arith/page.tsx index 392e95aca..6bd261831 100644 --- a/web/app/arith/page.tsx +++ b/web/app/arith/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { loadArithCorpus, arithmeticTopK, type ArithCorpus, type Term } from "@/lib/arith"; const DEFAULT_TERMS: Term[] = [ @@ -34,6 +35,23 @@ const EXAMPLES: { label: string; terms: Term[] }[] = [ }, ]; +// candidate analogies for "surprise me" — filtered at runtime to ones whose +// terms all exist in the corpus, so a shuffle never lands on a dead term. +const SURPRISES: Term[][] = [ + [{ sign: 1, word: "king" }, { sign: -1, word: "man" }, { sign: 1, word: "woman" }], + [{ sign: 1, word: "paris" }, { sign: -1, word: "france" }, { sign: 1, word: "germany" }], + [{ sign: 1, word: "paris" }, { sign: -1, word: "france" }, { sign: 1, word: "italy" }], + [{ sign: 1, word: "paris" }, { sign: -1, word: "france" }, { sign: 1, word: "japan" }], + [{ sign: 1, word: "better" }, { sign: -1, word: "good" }, { sign: 1, word: "bad" }], + [{ sign: 1, word: "bigger" }, { sign: -1, word: "big" }, { sign: 1, word: "small" }], + [{ sign: 1, word: "walking" }, { sign: -1, word: "walk" }, { sign: 1, word: "swim" }], + [{ sign: 1, word: "queen" }, { sign: -1, word: "woman" }, { sign: 1, word: "man" }], + [{ sign: 1, word: "london" }, { sign: -1, word: "england" }, { sign: 1, word: "spain" }], + [{ sign: 1, word: "summer" }, { sign: -1, word: "hot" }, { sign: 1, word: "cold" }], + [{ sign: 1, word: "doctor" }, { sign: -1, word: "hospital" }, { sign: 1, word: "school" }], + [{ sign: 1, word: "coffee" }, { sign: -1, word: "morning" }, { sign: 1, word: "night" }], +]; + const ARITH_YEAR = 2025; export default function ArithPage() { @@ -70,6 +88,29 @@ export default function ArithPage() { setTerms((prev) => [...prev, { sign: 1, word }]); } + // analogies whose every term is in the corpus — safe to shuffle through + const validSurprises = useMemo(() => { + if (!corpus) return []; + return SURPRISES.filter((t) => + t.every((x) => corpus.wordToIdx.has(x.word.toLowerCase())), + ); + }, [corpus]); + + function surpriseMe() { + if (validSurprises.length === 0) return; + const cur = terms.map((t) => `${t.sign}${t.word}`).join(","); + const pool = validSurprises.filter( + (t) => t.map((x) => `${x.sign}${x.word}`).join(",") !== cur, + ); + const pick = (pool.length ? pool : validSurprises)[ + Math.floor(Math.random() * (pool.length ? pool.length : validSurprises.length)) + ]; + setTerms(pick.map((t) => ({ ...t }))); + } + + const answer = results && results.length > 0 ? results[0] : null; + const others = results ? results.slice(1) : []; + return (
))} + {validSurprises.length > 0 && ( + + )}
{/* Equation */} @@ -127,9 +177,6 @@ export default function ArithPage() { {/* Results */}
-
- nearest in {ARITH_YEAR} -
{!corpus ? (
@@ -143,41 +190,77 @@ export default function ArithPage() {
one or more terms aren't in the vocabulary (need freq ≥ 3,000 in any year)
- ) : !results ? ( + ) : !answer ? (
add at least one term
) : ( -
    - {results.map((r, i) => { - const max = results[0]?.sim || 1; - const pct = Math.max(4, (r.sim / max) * 100); - return ( -
  1. -
    - - {i + 1} - - - - {r.sim.toFixed(3)} - -
    -
    -
    -
    -
  2. - ); - })} -
+ <> + {/* the answer */} +
+ nearest in {ARITH_YEAR} +
+
+ + + addAsTerm(answer.word)} + initial={{ opacity: 0, y: 10, filter: "blur(6px)" }} + animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} + exit={{ opacity: 0, y: -8, filter: "blur(6px)" }} + transition={{ duration: 0.45, ease: "easeOut" }} + className="font-display text-shimmer text-[clamp(40px,9vw,88px)] leading-none lowercase hover:opacity-90" + title="add to terms" + > + {answer.word} + + + + cos {answer.sim.toFixed(3)} + +
+ + {/* runners-up */} + {others.length > 0 && ( + <> +
+ others nearby +
+
    + {others.map((r, i) => { + const max = answer.sim || 1; + const pct = Math.max(4, (r.sim / max) * 100); + return ( +
  1. +
    + + {i + 2} + + + + {r.sim.toFixed(3)} + +
    +
    +
    +
    +
  2. + ); + })} +
+ + )} + )}