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. + ); + })} +
+ + )} + )}