mirror of
https://github.com/samdotmak/jev-recall.git
synced 2026-10-27 13:55:48 +01:00
Paper theme, per-request results strip, hover readouts, audit fixes
- warm paper/ink palette with serif display type and mono figures, sparing oxblood accent for brand marks and the memory that matters - entrance animations moved from JS to CSS so they never stall mid-recording - hover any grid square or result row to read the full memory - bottom strip now reports the selected request, falling back to the benchmark - editorial preset nav replaces chips; brand row and status hints removed - audit: genericize a persona detail that echoed the author's own school, add nosniff/referrer/permissions headers and no-store on API responses Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
053837b5a5
commit
e31c867618
@@ -2,65 +2,131 @@
|
||||
|
||||
**Retrieve by relevance, not resemblance.**
|
||||
|
||||
Give it a situation and a list of memories. It returns only the memories that matter,
|
||||
judged by TypeSafe's Jev model, one calibrated yes/no per memory, all in one request.
|
||||
Give it a situation and a pile of memories. It returns only the memories that matter, judged by
|
||||
[TypeSafe's Jev](https://typesafe.ai) model, one calibrated yes/no per memory, in a single request.
|
||||
|
||||
<!-- TODO: replace with the recorded demo GIF -->
|
||||

|
||||
|
||||
**[Try the live demo →](https://jev-recall.vercel.app)** · replays real runs, or runs live with your own API keys.
|
||||
|
||||
## The problem
|
||||
|
||||
With LLMs, writing memories is easy. Knowing which ones to *retrieve* is the hard part.
|
||||
|
||||
Today you get two options, and both make you guess in advance:
|
||||
|
||||
- **Keyword search** works only if you already know what you're looking for.
|
||||
- **Semantic search** works only if the memory that matters happens to look like the request.
|
||||
|
||||
That second assumption breaks constantly, because the memories that change an answer are usually
|
||||
the ones phrased nothing like the question. "Order the almond tart for Saturday's dinner party"
|
||||
looks nothing like "Leo has a severe tree-nut allergy," so embeddings return other pastry memories
|
||||
and the assistant orders the tart.
|
||||
|
||||
An LLM used as a re-ranker does catch those. It reads everything and reasons about what matters.
|
||||
But then you pay LLM prices and LLM latency on every single turn, so nobody runs it on every turn.
|
||||
|
||||
Jev is a classifier that makes that same include-or-not decision at LLM quality, for a fraction of
|
||||
the cost and time. That makes it cheap enough to judge *every* memory on *every* turn.
|
||||
|
||||
## How it works
|
||||
|
||||
Every memory goes into one request's shared `state`, tagged with a short id. Each memory gets a tiny
|
||||
yes/no question pointing at its id, and Jev returns a calibrated probability for each one. Because the
|
||||
probabilities are calibrated, you keep everything above a threshold instead of guessing at a top-k,
|
||||
and nothing is returned when nothing applies.
|
||||
|
||||
```python
|
||||
from jev_recall import Recall
|
||||
|
||||
recall = Recall() # reads TYPESAFE_API_KEY
|
||||
result = recall.filter(
|
||||
"Book a table for Dad's 70th birthday dinner on Saturday, Oct 3. 6 of us.",
|
||||
memories, # list[str] or list[Memory] or list[{"id", "text"}]
|
||||
threshold=0.5, # calibrated probability; no top-k guessing
|
||||
"Order the almond frangipane tart from Tartine for Saturday's dinner party.",
|
||||
memories, # list[str] | list[Memory] | list[{"id", "text"}]
|
||||
threshold=0.5,
|
||||
)
|
||||
result.texts # the relevant memories, best first (can be empty)
|
||||
result.texts # the memories that matter, best first (can be empty)
|
||||
result.all_scores # every memory with its probability
|
||||
result.cost_usd, result.latency_s
|
||||
```
|
||||
|
||||
Memories go into the request's state once, tagged with short ids, and each gets a tiny
|
||||
yes/no question that points at its id (`mode="pointer"`, the default). Stores too big for
|
||||
one request are split and sent concurrently. `afilter` is the async version.
|
||||
|
||||
Other modes: `"inline"` puts each memory's text in its own question, and `"shortlist"` ranks
|
||||
every memory with one multiple-choice question and then verifies the top 15.
|
||||
|
||||
## Demo site
|
||||
|
||||
`web/` is a Next.js demo that races OpenAI embeddings, Claude Sonnet 5 and Jev Recall on
|
||||
personal-assistant requests. It replays recorded runs by default, and visitors can run it
|
||||
live with their own API keys. See [web/README.md](web/README.md).
|
||||
Large stores are split across requests and sent concurrently. `afilter` is the async version.
|
||||
Two other modes are available: `inline` puts each memory's text in its own question, and `shortlist`
|
||||
ranks everything with one multiple-choice question and then verifies the top candidates.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`bench/` holds one assistant's memory store (238 memories) and 18 requests where the
|
||||
memory that matters doesn't look like the request: a quiet-period rule when drafting a
|
||||
LinkedIn post, a stolen card when paying a bill, a pregnancy when booking a dive trip.
|
||||
`bench/` holds one assistant's memory store of 238 fictional memories and 18 requests where the
|
||||
memory that matters doesn't look like the request: a stolen card when paying a bill, a pregnancy
|
||||
when booking a dive trip, a guest's nut allergy when ordering dessert.
|
||||
|
||||
```bash
|
||||
cp .env.example .env # add TYPESAFE_API_KEY and ANTHROPIC_API_KEY
|
||||
uv run --group bench python bench/run.py --systems bm25,dense,dense+rerank,openai
|
||||
uv run --group bench python bench/run.py --systems jev-inline,jev-pointer,jev-shortlist
|
||||
uv run --group bench python bench/run.py --systems llm:claude-haiku-4-5,llm:claude-sonnet-5,llm:claude-opus-5
|
||||
```
|
||||
|
||||
Results land in `bench/results/`. Run on 2026-09-19 with `jev-1.13.0`:
|
||||
Run on 2026-09-19 with `jev-1.13.0`. "Key memories" counts the 20 memories across those requests that
|
||||
an assistant had to know about before acting.
|
||||
|
||||
| System | Requests passed | Key memories found | Cost per request | Latency |
|
||||
|---|---|---|---|---|
|
||||
| Keyword search (BM25), top 5 | 1/18 | 2/20 | local | <0.01s |
|
||||
| bge-large embeddings, top 5 | 3/18 | 4/20 | local | 0.06s |
|
||||
| bge-large + bge-reranker, top 5 | 1/18 | 1/20 | local | 1.2s |
|
||||
| OpenAI text-embedding-3-large, top 5 | 3/18 | 4/20 | <$0.00001 | 0.4s |
|
||||
| **Jev Recall, pointer mode** | **17/18** | **19/20** | **$0.00044** | **0.35s** |
|
||||
| OpenAI text-embedding-3-large, top 5 | 3/18 | 4/20 | $0.000002 | 0.4s |
|
||||
| **Jev Recall (pointer mode)** | **17/18** | **19/20** | **$0.00044** | **0.35s** |
|
||||
| Claude Haiku 4.5 | 14/18 | 15/20 | $0.0055 | 1.4s |
|
||||
| Claude Sonnet 5 | 17/18 | 19/20 | $0.020 | 7.6s |
|
||||
| Claude Opus 5 | 18/18 | 20/20 | $0.049 | 7.6s |
|
||||
|
||||
Claude costs are without prompt caching. With the memory store cached, Sonnet 5 costs
|
||||
$0.0085 and Opus 5 costs $0.019 per request.
|
||||
Jev matches Sonnet 5 and is within one memory of Opus 5, at roughly 1/46 of Sonnet's cost and 20x its
|
||||
speed. Scores move by less than 0.01 between runs, and the result holds at any threshold from 0.3 to 0.5.
|
||||
|
||||
Two caveats worth stating plainly. Claude's costs above are without prompt caching; with the memory
|
||||
store cached, Sonnet 5 costs $0.0085 and Opus 5 costs $0.019 per request, so the cost gap narrows to
|
||||
about 19x and 43x. And Jev's one miss is a memory that needs outside knowledge to connect ("Sam is on
|
||||
an H-1B visa" rules out paid freelance work). Sonnet missed that one too.
|
||||
|
||||
## The demo
|
||||
|
||||
`web/` is the Next.js site behind [jev-recall.vercel.app](https://jev-recall.vercel.app). Three methods
|
||||
race across one shared grid of all 238 memories: OpenAI embeddings, Claude Sonnet 5 and Jev Recall.
|
||||
Each scanner settles on what its method picked, and a method that misses the memory that mattered says so.
|
||||
|
||||
```bash
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
It replays recorded real runs by default. **Run live with your keys** accepts your own TypeSafe,
|
||||
Anthropic and OpenAI keys; they stay in your browser's local storage and are passed straight through
|
||||
to the provider by the site's API route, which stores and logs nothing. The deployment holds no keys
|
||||
of its own. `/?present=1` autoplays once with the controls hidden, for screen recordings.
|
||||
|
||||
## Repo layout
|
||||
|
||||
| Path | What's in it |
|
||||
|---|---|
|
||||
| `src/jev_recall/` | the library: request building, sharding, retries, scoring |
|
||||
| `tests/` | unit tests against a mocked Jev API (`uv run pytest`) |
|
||||
| `bench/data/` | the 238 fictional memories and the 18 labeled requests |
|
||||
| `bench/run.py` | the benchmark harness and every baseline |
|
||||
| `web/` | the demo site |
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
uv sync # library + dev deps
|
||||
uv sync --group bench # baselines: fastembed, rank-bm25, anthropic, openai
|
||||
cp .env.example .env # TYPESAFE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY
|
||||
```
|
||||
|
||||
A TypeSafe key is all the library itself needs. The other two are only for benchmark baselines and for
|
||||
recording the demo's replays.
|
||||
|
||||
## License
|
||||
|
||||
MIT. The memories in `bench/data` describe a fictional person.
|
||||
MIT. The memories in `bench/data` describe a fictional person; any resemblance to real people is coincidental.
|
||||
|
||||
@@ -155,7 +155,7 @@ mom_garden | Mom's roses have won ribbons at the county fair.
|
||||
sister | User's younger sister Maya lives in Brooklyn and works in publishing.
|
||||
maya_visit | Maya is visiting for a week in early November.
|
||||
college | User went to UCLA and studied economics.
|
||||
mba | User did an MBA at Kellogg.
|
||||
mba | User did an MBA a few years after college.
|
||||
languages | User speaks English and conversational Spanish.
|
||||
volunteering | User volunteers at the SF-Marin Food Bank once a month.
|
||||
donation | User gives monthly to KQED.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"when": "2026-09-19T16:50:53",
|
||||
"k": 5,
|
||||
"threshold": 0.5,
|
||||
"n_memories": 238,
|
||||
"results": {
|
||||
"openai": [
|
||||
{
|
||||
"scenario": "none_math",
|
||||
"got": [
|
||||
"parking",
|
||||
"q3_okr",
|
||||
"pto",
|
||||
"laptop",
|
||||
"alarm"
|
||||
],
|
||||
"found": [],
|
||||
"missed": [],
|
||||
"noise": [
|
||||
"parking",
|
||||
"q3_okr",
|
||||
"pto",
|
||||
"laptop",
|
||||
"alarm"
|
||||
],
|
||||
"returned": 5,
|
||||
"pass": false,
|
||||
"latency_s": 0.5623054999887245,
|
||||
"cost_usd": 1.17e-06
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Put `demo.gif` here; the root README links to `docs/demo.gif`.
|
||||
+5
-3
@@ -1,8 +1,10 @@
|
||||
# Jev Recall demo
|
||||
|
||||
A single-page demo: an assistant knows 238 things about your life, and three systems pick which
|
||||
ones it should read before acting. Semantic search (OpenAI embeddings), an LLM judge
|
||||
(Claude Sonnet 5) and Jev Recall run side by side.
|
||||
Three methods race across one shared grid of all 238 memories: semantic search (OpenAI
|
||||
embeddings), Claude Sonnet 5 as an LLM judge, and Jev Recall. Each scanner settles on what its
|
||||
method picked, and a method that misses the memory that mattered says so.
|
||||
|
||||
Live at https://jev-recall.vercel.app
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
|
||||
+56
-18
@@ -1,20 +1,21 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Light "scientific instrument" theme from the Shared Scanfield handoff.
|
||||
Method colors are fixed across scanners, timers and result cards. */
|
||||
/* Warm paper-and-ink palette: off-white stock, near-black ink, serif display,
|
||||
monospace for labels and figures. Method colors are muted to match. */
|
||||
:root {
|
||||
--bg: #f6f7f9;
|
||||
--panel: #ffffff;
|
||||
--panel-2: #f8fafc;
|
||||
--cell: #eceef2;
|
||||
--line: rgba(15, 23, 42, 0.08);
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--faint: #94a3b8;
|
||||
--embeddings: #3b82f6;
|
||||
--sonnet: #f97316;
|
||||
--jev: #14b8a6;
|
||||
--jev-ink: #0f766e;
|
||||
--bg: #f1eee5;
|
||||
--panel: #faf8f2;
|
||||
--panel-2: #f4f1e8;
|
||||
--cell: #e3dfd2;
|
||||
--line: rgba(22, 21, 18, 0.14);
|
||||
--text: #16150f;
|
||||
--muted: #6d6758;
|
||||
--faint: #9b9484;
|
||||
--embeddings: #3a6ea5;
|
||||
--sonnet: #c2613a;
|
||||
--jev: #2f7f6b;
|
||||
--jev-ink: #235f50;
|
||||
--accent: #9c2f26; /* oxblood: brand marks and "the memory that matters" */
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -30,8 +31,10 @@
|
||||
--color-sonnet: var(--sonnet);
|
||||
--color-jev: var(--jev);
|
||||
--color-jev-ink: var(--jev-ink);
|
||||
--font-sans: var(--font-onest);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-accent: var(--accent);
|
||||
--font-sans: var(--font-serif);
|
||||
--font-serif: var(--font-serif);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -39,11 +42,21 @@ body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-onest), system-ui, sans-serif;
|
||||
font-family: var(--font-serif), Georgia, serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Small uppercase monospace labels, as on the reference spec sheet. */
|
||||
.label {
|
||||
font-family: var(--font-mono), ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tabular {
|
||||
font-family: var(--font-mono), ui-monospace, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -65,7 +78,32 @@ body {
|
||||
}
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--cell) 0px, #f8fafc 80px, var(--cell) 160px);
|
||||
background: linear-gradient(90deg, var(--cell) 0px, var(--panel-2) 80px, var(--cell) 160px);
|
||||
background-size: 400px 100%;
|
||||
animation: shimmer 1.2s linear infinite;
|
||||
}
|
||||
|
||||
/* Entrance animations run as CSS, not JS: they keep playing even when the
|
||||
rAF-driven animation loop is throttled (long screen recordings, background tabs). */
|
||||
@keyframes fade-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.fade-up {
|
||||
animation: fade-up 0.42s cubic-bezier(0.2, 0.7, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
.pulse-dot {
|
||||
animation: pulse-dot 0.9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist_Mono, Onest } from "next/font/google";
|
||||
import { IBM_Plex_Mono, Newsreader } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const onest = Onest({ variable: "--font-onest", subsets: ["latin"] });
|
||||
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
|
||||
const serif = Newsreader({ variable: "--font-serif", subsets: ["latin"], style: ["normal", "italic"] });
|
||||
const mono = IBM_Plex_Mono({ variable: "--font-mono", subsets: ["latin"], weight: ["400", "500"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Jev Recall · See what each method finds",
|
||||
title: "Jev Recall · Catches the memories semantic search misses",
|
||||
description:
|
||||
"An AI assistant knows 238 things about your life. Watch semantic search, Claude Sonnet 5 and Jev Recall pick which ones matter.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html lang="en" className={`${onest.variable} ${geistMono.variable} antialiased`}>
|
||||
<html lang="en" className={`${serif.variable} ${mono.variable} antialiased`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+96
-59
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Preset } from "@/data/presets";
|
||||
import { METHOD_COLOR } from "@/lib/format";
|
||||
import { KEY_HEADERS, SYSTEMS, type Memory, type RunResult, type SystemId } from "@/lib/types";
|
||||
import { KeysDialog, useKeys } from "./KeysDialog";
|
||||
import { ResultCard, type CardState } from "./ResultCard";
|
||||
@@ -19,7 +20,14 @@ const IDLE: Cards = {
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// From the benchmark in bench/ (README): 238 memories, 18 requests, 20 key memories.
|
||||
const BENCHMARK = { jev: "19/20", sonnet: "19/20", embeddings: "4/20", faster: "~22× faster", cost: "~1/46 cost" };
|
||||
const BENCHMARK = {
|
||||
requests: 18,
|
||||
jev: "19/20",
|
||||
sonnet: "19/20",
|
||||
embeddings: "4/20",
|
||||
faster: "~22× faster",
|
||||
cost: "~1/46 the cost",
|
||||
};
|
||||
|
||||
export function Demo({
|
||||
memories,
|
||||
@@ -171,32 +179,55 @@ export function Demo({
|
||||
}),
|
||||
) as Record<SystemId, Scan>;
|
||||
|
||||
const done = phase === "done";
|
||||
const sonnetRun = cards.sonnet.result;
|
||||
const jevRun = cards.jev.result;
|
||||
const runStats =
|
||||
done && sonnetRun && jevRun && keyIds.length > 0
|
||||
? {
|
||||
caption: `${keyIds.length === 1 ? "1 key memory" : `${keyIds.length} key memories`} to find`,
|
||||
found: Object.fromEntries(
|
||||
SYSTEMS.map(({ id }) => {
|
||||
const r = cards[id].result;
|
||||
const hit = r ? keyIds.filter((k) => r.ids.includes(k)).length : 0;
|
||||
return [id, `${hit}/${keyIds.length}`];
|
||||
}),
|
||||
) as Record<SystemId, string>,
|
||||
faster: `${Math.round(cards.sonnet.elapsedMs / Math.max(cards.jev.elapsedMs, 1))}× faster`,
|
||||
cost: `1/${Math.round(sonnetRun.costUsd / Math.max(jevRun.costUsd, 1e-9))} the cost`,
|
||||
}
|
||||
: null;
|
||||
|
||||
const headline =
|
||||
phase === "running" ? `Scanning ${memories.length} memories…` : phase === "done" ? `Scanned ${memories.length} memories` : `${memories.length} memories`;
|
||||
const recordedAt = replays[preset.id]?.recordedAt;
|
||||
const editable = live && !present && phase !== "running" && phase !== "typing";
|
||||
|
||||
return (
|
||||
<Stage>
|
||||
<div className="flex h-full flex-col px-10 pb-7 pt-6">
|
||||
{/* Brand */}
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg border-[1.5px] border-[#cbd2dc] bg-white text-[17px] font-medium text-text">
|
||||
J
|
||||
<div className="flex h-full flex-col px-10 pb-7 pt-8">
|
||||
{/* Headline + mode controls */}
|
||||
<div className="flex items-start justify-between gap-8">
|
||||
<div>
|
||||
<div className="mb-3 flex gap-[3px]">
|
||||
<span className="h-[5px] w-[22px] rounded-full bg-embeddings" />
|
||||
<span className="h-[5px] w-[22px] rounded-full bg-sonnet" />
|
||||
<span className="h-[5px] w-[22px] rounded-full bg-jev" />
|
||||
<span className="h-[5px] w-[9px] rounded-full bg-accent" />
|
||||
</div>
|
||||
<div className="text-[19px] font-medium text-text">Jev Recall</div>
|
||||
<h1 className="text-[40px] font-semibold leading-[1.08] tracking-[-0.01em] text-text">
|
||||
<span className="rounded-[4px] bg-jev/15 px-1.5 pb-0.5 decoration-clone">Jev Recall</span> catches the
|
||||
memories semantic search{" "}
|
||||
<em className="underline decoration-accent decoration-[3px] underline-offset-[6px]">misses.</em>
|
||||
</h1>
|
||||
<p className="mt-2 text-[19px] leading-snug text-muted">
|
||||
Same answers as Claude Sonnet 5, at a fraction of the time and cost.
|
||||
</p>
|
||||
</div>
|
||||
{!present && (
|
||||
<div className="flex items-center gap-3 text-[13px]">
|
||||
<span className="text-faint">
|
||||
{live
|
||||
? "Live with your keys"
|
||||
: `Replaying a live run${recordedAt ? ` from ${new Date(recordedAt + "T12:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric" })}` : ""}`}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-3 pt-2">
|
||||
<button
|
||||
onClick={() => setShowKeys(true)}
|
||||
className="rounded-full border border-line bg-white px-3.5 py-1.5 text-text shadow-sm hover:border-[#cbd2dc]"
|
||||
className="label rounded-full border border-line bg-panel px-3.5 py-1.5 !text-text hover:bg-panel-2"
|
||||
>
|
||||
{live ? "Keys" : "Run live with your keys"}
|
||||
</button>
|
||||
@@ -204,16 +235,36 @@ export function Demo({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="mt-2 text-[42px] font-bold leading-[1.1] tracking-[-0.02em] text-text">
|
||||
See what each method <span className="text-faint">finds.</span>
|
||||
</h1>
|
||||
{/* Preset nav */}
|
||||
{!present && (
|
||||
<div className="mt-6 border-b border-line pb-2.5">
|
||||
<div className="flex items-baseline gap-7">
|
||||
{presets.map((p) => {
|
||||
const active = p.id === preset.id && isPreset;
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => choose(p, false)}
|
||||
className={`relative pb-2.5 text-[16px] transition ${active ? "text-text" : "text-muted hover:text-text"}`}
|
||||
>
|
||||
{p.label}
|
||||
{active && <span className="absolute inset-x-0 -bottom-[1px] h-[2px] rounded-full bg-accent" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request */}
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="flex h-[52px] w-[52px] shrink-0 items-center justify-center rounded-full border border-line bg-white text-[15px] text-muted">
|
||||
You
|
||||
<div className="mt-5 flex items-center gap-4">
|
||||
<div className="flex h-[62px] w-[62px] shrink-0 items-center justify-center rounded-xl border border-line bg-panel text-accent">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" aria-label="Your request" role="img">
|
||||
<circle cx="12" cy="8" r="3.6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M4.8 20c.6-3.8 3.6-6 7.2-6s6.6 2.2 7.2 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex h-[62px] flex-1 items-center rounded-2xl border border-[#dde2e9] bg-white px-6 shadow-[0_1px_2px_rgba(15,23,42,0.04)]">
|
||||
<div className="flex h-[62px] flex-1 items-center rounded-xl border border-line bg-panel px-6">
|
||||
{editable ? (
|
||||
<input
|
||||
value={request}
|
||||
@@ -237,7 +288,7 @@ export function Demo({
|
||||
<button
|
||||
onClick={() => run(preset, request, { type: false })}
|
||||
disabled={phase === "running" || phase === "typing" || !request.trim()}
|
||||
className="flex h-[62px] shrink-0 items-center gap-3 rounded-2xl border border-[#99e6d8] bg-[#d5f5ef] px-7 text-[19px] font-semibold text-text transition hover:bg-[#c3f0e7] disabled:opacity-60"
|
||||
className="flex h-[62px] shrink-0 items-center gap-3 rounded-xl bg-text px-8 text-[19px] font-medium text-[#faf8f2] transition hover:opacity-90 disabled:opacity-40"
|
||||
>
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" aria-hidden>
|
||||
<path d="M1 1.5v13l12-6.5z" fill="currentColor" />
|
||||
@@ -245,25 +296,6 @@ export function Demo({
|
||||
Run comparison
|
||||
</button>
|
||||
</div>
|
||||
{!present && (
|
||||
<div className="ml-[68px] mt-2.5 flex gap-2">
|
||||
{presets.map((p, i) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => choose(p, false)}
|
||||
className={`rounded-full border px-3 py-1 text-[13px] transition ${
|
||||
p.id === preset.id && isPreset
|
||||
? "border-[#cbd2dc] bg-white text-text shadow-sm"
|
||||
: "border-transparent text-muted hover:text-text"
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1.5 text-faint">{i + 1}</span>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scan field + results */}
|
||||
<div className="mt-4 grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_600px] gap-5">
|
||||
<ScanField
|
||||
@@ -291,28 +323,33 @@ export function Demo({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Benchmark strip */}
|
||||
<div className="mt-4 flex h-[60px] items-center rounded-2xl border border-line bg-panel px-8 text-[20px]">
|
||||
{/* Results strip: this run once it finishes, the full benchmark before that */}
|
||||
<div className="mt-4 flex h-[60px] items-center rounded-xl border border-line bg-panel px-8 text-[19px]">
|
||||
<div className="flex items-center gap-3 pr-8">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" aria-hidden className="text-jev">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden className="text-accent">
|
||||
<path d="M7 4h10v5a5 5 0 0 1-10 0V4Z M7 6H4a3 3 0 0 0 3 4 M17 6h3a3 3 0 0 1-3 4 M12 14v4 M8 20h8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className="text-text">Benchmark</span>
|
||||
<span className="text-muted">(same {memories.length} memories)</span>
|
||||
<span className="label !text-text">{runStats ? "This request" : "Benchmark"}</span>
|
||||
<span className="text-[15px] text-muted">
|
||||
{runStats ? runStats.caption : `${BENCHMARK.requests} requests, ${memories.length} memories`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center gap-10 border-l border-line pl-10">
|
||||
<span className="text-muted">
|
||||
Jev <span className="ml-2 text-[24px] font-medium text-jev">{BENCHMARK.jev}</span>
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
Sonnet <span className="ml-2 text-[24px] font-medium text-sonnet">{BENCHMARK.sonnet}</span>
|
||||
</span>
|
||||
<span className="text-muted">
|
||||
embeddings <span className="ml-2 text-[24px] font-medium text-embeddings">{BENCHMARK.embeddings}</span>
|
||||
</span>
|
||||
{SYSTEMS.map(({ id }) => (
|
||||
<span key={id} className="text-muted">
|
||||
{id === "sonnet" ? "Sonnet" : id === "embeddings" ? "Embeddings" : "Jev"}{" "}
|
||||
<span className="tabular ml-2 text-[21px]" style={{ color: METHOD_COLOR[id] }}>
|
||||
{runStats ? runStats.found[id] : BENCHMARK[id]}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="tabular border-l border-line px-10 text-[17px] text-text">
|
||||
{runStats ? runStats.faster : BENCHMARK.faster}
|
||||
</div>
|
||||
<div className="tabular border-l border-line pl-10 text-[17px] text-text">
|
||||
{runStats ? runStats.cost : BENCHMARK.cost}
|
||||
</div>
|
||||
<div className="border-l border-line px-10 text-text">{BENCHMARK.faster}</div>
|
||||
<div className="border-l border-line pl-10 text-text">{BENCHMARK.cost}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showKeys && <KeysDialog initial={keys} onClose={() => setShowKeys(false)} />}
|
||||
|
||||
@@ -59,8 +59,8 @@ export function KeysDialog({ initial, onClose }: { initial: Keys; onClose: () =>
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-slate-900/30 backdrop-blur-sm" onClick={() => onClose()}>
|
||||
<div className="w-[560px] rounded-2xl border border-line bg-panel p-7 shadow-2xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-[#16150f]/25 backdrop-blur-sm" onClick={() => onClose()}>
|
||||
<div className="w-[560px] rounded-xl border border-line bg-panel p-7 shadow-2xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="text-[22px] font-semibold">Run it live with your own keys</div>
|
||||
<p className="mt-2 text-[14px] leading-relaxed text-muted">
|
||||
Without keys you see recorded live runs. Add all three keys to run the presets live or type your own request.
|
||||
@@ -81,7 +81,7 @@ export function KeysDialog({ initial, onClose }: { initial: Keys; onClose: () =>
|
||||
value={keys[f.id]}
|
||||
placeholder={f.placeholder}
|
||||
onChange={(e) => setKeys({ ...keys, [f.id]: e.target.value.trim() })}
|
||||
className="mt-1.5 w-full rounded-lg border border-line bg-panel-2 px-3 py-2 font-mono text-[13px] text-text outline-none focus:border-jev"
|
||||
className="mt-1.5 w-full rounded-lg border border-line bg-panel-2 px-3 py-2 font-mono text-[13px] text-text outline-none focus:border-text"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
@@ -94,7 +94,7 @@ export function KeysDialog({ initial, onClose }: { initial: Keys; onClose: () =>
|
||||
<button onClick={() => done({ ...EMPTY_KEYS })} className="text-[14px] text-muted hover:text-text">
|
||||
Forget keys
|
||||
</button>
|
||||
<button onClick={() => done(keys)} className="rounded-lg bg-text px-5 py-2 text-[14px] font-semibold text-white">
|
||||
<button onClick={() => done(keys)} className="rounded-lg bg-text px-5 py-2 text-[15px] text-[#faf8f2]">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { METHOD_COLOR, formatCost } from "@/lib/format";
|
||||
import type { Memory, RunResult, SystemId } from "@/lib/types";
|
||||
|
||||
@@ -62,14 +61,14 @@ export function ResultCard({
|
||||
].slice(0, ROWS);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col rounded-2xl border border-line bg-panel px-5 py-3.5">
|
||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col rounded-xl border border-line bg-panel px-5 py-3.5">
|
||||
<div className="flex items-start">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-[18px] w-[18px] rounded-full" style={{ background: color }} />
|
||||
<span className="text-[19px] font-semibold tracking-tight text-text">{name}</span>
|
||||
<span className="text-[20px] font-semibold text-text">{name}</span>
|
||||
</div>
|
||||
<div className="ml-[30px] mt-0.5 text-[13px] text-muted">
|
||||
<div className="ml-[30px] mt-0.5 text-[14px] text-muted">
|
||||
{method}
|
||||
{r && (
|
||||
<span className="ml-2 text-text">
|
||||
@@ -82,14 +81,14 @@ export function ResultCard({
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="w-[86px] border-l border-line pl-4">
|
||||
<div className="text-[13px] text-muted">Time</div>
|
||||
<div className="tabular text-[19px] text-text">
|
||||
<div className="label">Time</div>
|
||||
<div className="tabular mt-0.5 text-[18px] text-text">
|
||||
{state.status === "idle" ? "–" : `${(state.elapsedMs / 1000).toFixed(2)}s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[118px] border-l border-line pl-4">
|
||||
<div className="text-[13px] text-muted">Cost</div>
|
||||
<div className="tabular text-[19px] text-text">{r ? formatCost(r.costUsd) : "–"}</div>
|
||||
<div className="label">Cost</div>
|
||||
<div className="tabular mt-0.5 text-[18px] text-text">{r ? formatCost(r.costUsd) : "–"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,49 +96,57 @@ export function ResultCard({
|
||||
<div className="mt-2.5 flex min-h-0 flex-1 flex-col gap-1.5">
|
||||
{state.status === "running" &&
|
||||
[0, 1].map((i) => <div key={i} className="skeleton h-[42px] shrink-0 rounded-xl" />)}
|
||||
<AnimatePresence>
|
||||
{rows.map((row, i) => {
|
||||
{rows.map((row, i) => {
|
||||
const score = row.kind !== "miss" ? r?.scores?.[row.id] : undefined;
|
||||
const tinted = row.kind === "hit";
|
||||
const full = row.kind === "miss" ? keyIds.map((k) => memoryById[k]?.text).join(" ") : memoryById[row.id]?.text;
|
||||
return (
|
||||
<motion.div
|
||||
<div
|
||||
key={`${id}-${row.id}`}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.08 + i * 0.12 }}
|
||||
className="flex h-[42px] shrink-0 items-center gap-3 rounded-xl border px-3"
|
||||
className="fade-up group relative flex h-[42px] shrink-0 items-center gap-3 rounded-xl border px-3"
|
||||
style={{
|
||||
animationDelay: `${80 + i * 120}ms`,
|
||||
borderColor: tinted ? `color-mix(in srgb, ${color} 30%, transparent)` : "var(--line)",
|
||||
background: tinted ? `color-mix(in srgb, ${color} 9%, white)` : row.kind === "miss" ? "var(--panel-2)" : "white",
|
||||
background: tinted
|
||||
? `color-mix(in srgb, ${color} 12%, var(--panel))`
|
||||
: row.kind === "miss"
|
||||
? "var(--panel-2)"
|
||||
: "var(--panel)",
|
||||
}}
|
||||
>
|
||||
{full && (
|
||||
<div
|
||||
className={`pointer-events-none absolute inset-x-0 z-50 rounded-lg border border-line bg-panel px-3 py-2 text-[14px] leading-snug text-text opacity-0 shadow-[0_8px_24px_rgba(22,21,15,0.14)] transition-opacity duration-150 group-hover:opacity-100 ${
|
||||
i === 0 ? "top-[calc(100%+6px)]" : "bottom-[calc(100%+6px)]"
|
||||
}`}
|
||||
>
|
||||
{full}
|
||||
</div>
|
||||
)}
|
||||
{row.kind === "miss" ? <MissIcon /> : <DocIcon />}
|
||||
<div className="min-w-0 flex-1">
|
||||
{row.kind === "miss" ? (
|
||||
<>
|
||||
<div className="truncate text-[14px] text-muted">
|
||||
<div className="truncate text-[15px] text-muted">
|
||||
{keyLabel.charAt(0).toUpperCase() + keyLabel.slice(1)}
|
||||
<span className="ml-2 text-[13px] text-faint">Doesn't find this detail.</span>
|
||||
<span className="ml-2 text-[14px] text-faint">Doesn't find this detail.</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="truncate text-[14px] text-text">{memoryById[row.id]?.text}</div>
|
||||
<div className="truncate text-[15px] text-text">{memoryById[row.id]?.text}</div>
|
||||
)}
|
||||
</div>
|
||||
{score !== undefined && tinted && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 + i * 0.12 }}
|
||||
className="tabular shrink-0 text-[14px] font-medium text-jev-ink"
|
||||
<span
|
||||
className="fade-up tabular shrink-0 text-[13px] text-jev-ink"
|
||||
style={{ animationDelay: `${500 + i * 120}ms` }}
|
||||
>
|
||||
Relevance {score.toFixed(2)}
|
||||
</motion.span>
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { METHOD_COLOR } from "@/lib/format";
|
||||
import { SYSTEMS, type Memory, type SystemId } from "@/lib/types";
|
||||
|
||||
@@ -38,6 +38,7 @@ export function ScanField({
|
||||
elapsedMs: number;
|
||||
running: boolean;
|
||||
}) {
|
||||
const [hover, setHover] = useState<number | null>(null);
|
||||
const index = Object.fromEntries(memories.map((m, i) => [m.id, i]));
|
||||
const rows = Math.ceil(memories.length / COLS);
|
||||
const width = (COLS - 1) * PITCH + CELL;
|
||||
@@ -53,29 +54,26 @@ export function ScanField({
|
||||
for (const id of scans[s.id].picks) (pickedBy[id] ??= []).push(s.id);
|
||||
}
|
||||
|
||||
const hovered = hover !== null ? memories[hover] : null;
|
||||
const hoverPos = hover !== null ? cellXY(hover) : null;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-2xl border border-line bg-panel px-8 pb-7 pt-6">
|
||||
<div className="flex h-full flex-col rounded-xl border border-line bg-panel px-8 pb-7 pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-[21px] text-text">{headline}</span>
|
||||
<span className="h-2 w-2 rounded-full bg-[#d8dce3]" />
|
||||
<span className="tabular font-mono text-[24px] font-semibold text-text">{(elapsedMs / 1000).toFixed(2)}s</span>
|
||||
{running && (
|
||||
<motion.span
|
||||
className="h-2 w-2 rounded-full bg-jev"
|
||||
animate={{ opacity: [1, 0.2, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 0.9 }}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[22px] text-text">{headline}</span>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-faint" />
|
||||
<span className="tabular text-[23px] text-text">{(elapsedMs / 1000).toFixed(2)}s</span>
|
||||
{running && <span className="pulse-dot h-1.5 w-1.5 rounded-full bg-jev" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[14px] text-faint">
|
||||
<div className="label flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 rounded-[3px] bg-cell" />
|
||||
Each square is a memory
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="relative" style={{ width, height }}>
|
||||
<div className="relative" style={{ width, height }} onMouseLeave={() => setHover(null)}>
|
||||
{/* cells */}
|
||||
{memories.map((m, i) => {
|
||||
const { x, y } = cellXY(i);
|
||||
@@ -83,15 +81,19 @@ export function ScanField({
|
||||
return (
|
||||
<div
|
||||
key={m.id}
|
||||
title={m.text}
|
||||
className="absolute rounded-[6px] transition-colors duration-500"
|
||||
onMouseEnter={() => setHover(i)}
|
||||
className="absolute rounded-[6px] transition-colors duration-300 hover:brightness-95"
|
||||
style={{
|
||||
left: x,
|
||||
top: y,
|
||||
width: CELL,
|
||||
height: CELL,
|
||||
background: isKey ? "#fff" : "var(--cell)",
|
||||
boxShadow: isKey ? "0 0 0 2px var(--text)" : undefined,
|
||||
background: isKey ? "var(--panel)" : "var(--cell)",
|
||||
boxShadow: isKey
|
||||
? "0 0 0 2px var(--accent), 0 0 12px color-mix(in srgb, var(--accent) 40%, transparent)"
|
||||
: hover === i
|
||||
? "0 0 0 2px var(--faint)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{pickedBy[m.id] && (
|
||||
@@ -117,9 +119,9 @@ export function ScanField({
|
||||
const scan = scans[id];
|
||||
if (scan.status === "idle") return null;
|
||||
const color = METHOD_COLOR[id];
|
||||
let pos: { x: number; y: number };
|
||||
let col = 0;
|
||||
const sweeping = scan.status === "running";
|
||||
let pos: { x: number; y: number } | null;
|
||||
let col = 0;
|
||||
if (sweeping) {
|
||||
const p = Math.floor((scan.elapsedMs / 1000) * SWEEP_CELLS_PER_SEC) + START_ROW[id] * COLS;
|
||||
const cell = p % memories.length;
|
||||
@@ -127,9 +129,10 @@ export function ScanField({
|
||||
col = cell % COLS;
|
||||
} else {
|
||||
const t = scan.target !== undefined ? index[scan.target] : undefined;
|
||||
pos = t !== undefined ? cellXY(t) : { x: -999, y: -999 };
|
||||
pos = t !== undefined ? cellXY(t) : null;
|
||||
}
|
||||
// Two scanners can settle on the same memory; nest them so both stay visible.
|
||||
if (!pos) return null;
|
||||
// Two scanners can settle on the same memory; nest one inside the other.
|
||||
const inset = !sweeping && id === "sonnet" && scans.jev.target === scan.target ? -5 : 0;
|
||||
return (
|
||||
<div key={id}>
|
||||
@@ -141,51 +144,62 @@ export function ScanField({
|
||||
left: Math.max(0, col - TRAIL) * PITCH,
|
||||
width: Math.min(col, TRAIL) * PITCH + CELL,
|
||||
height: CELL,
|
||||
background: `linear-gradient(90deg, transparent, color-mix(in srgb, ${color} 28%, transparent))`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pos.x > -999 && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute rounded-[8px] border-[3px]"
|
||||
initial={false}
|
||||
animate={{ x: pos.x - 4 + inset, y: pos.y - 4 + inset }}
|
||||
transition={sweeping ? { duration: 0 } : { type: "spring", stiffness: 140, damping: 18 }}
|
||||
style={{
|
||||
width: CELL + 8 - inset * 2,
|
||||
height: CELL + 8 - inset * 2,
|
||||
borderColor: color,
|
||||
background: inset ? "transparent" : `color-mix(in srgb, ${color} 22%, transparent)`,
|
||||
boxShadow: `0 0 16px color-mix(in srgb, ${color} 45%, transparent)`,
|
||||
zIndex: id === "jev" ? 3 : 2,
|
||||
background: `linear-gradient(90deg, transparent, color-mix(in srgb, ${color} 30%, transparent))`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="pointer-events-none absolute left-0 top-0 rounded-[8px] border-[3px]"
|
||||
style={{
|
||||
transform: `translate3d(${pos.x - 4 + inset}px, ${pos.y - 4 + inset}px, 0)`,
|
||||
transition: sweeping ? "none" : "transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
width: CELL + 8 - inset * 2,
|
||||
height: CELL + 8 - inset * 2,
|
||||
borderColor: color,
|
||||
background: inset ? "transparent" : `color-mix(in srgb, ${color} 20%, transparent)`,
|
||||
boxShadow: `0 0 14px color-mix(in srgb, ${color} 40%, transparent)`,
|
||||
zIndex: id === "jev" ? 3 : 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* hover readout */}
|
||||
{hovered && hoverPos && (
|
||||
<div
|
||||
className="pointer-events-none absolute z-20 max-w-[420px] rounded-lg border border-line bg-panel px-3 py-2 text-[14px] leading-snug text-text shadow-[0_8px_24px_rgba(22,21,15,0.14)]"
|
||||
style={{
|
||||
left: Math.min(Math.max(hoverPos.x + CELL / 2, 210), width - 210),
|
||||
top: hoverPos.y > 70 ? hoverPos.y - 12 : hoverPos.y + CELL + 12,
|
||||
transform: `translate(-50%, ${hoverPos.y > 70 ? "-100%" : "0"})`,
|
||||
}}
|
||||
>
|
||||
{hovered.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* the memory that matters, labeled once every method has reported */}
|
||||
{reveal &&
|
||||
!hovered &&
|
||||
(() => {
|
||||
const i = index[keyIds[0]];
|
||||
if (i === undefined) return null;
|
||||
const { x, y } = cellXY(i);
|
||||
const above = y > 60;
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: above ? 6 : -6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute z-10 whitespace-nowrap rounded-lg bg-text px-3 py-1.5 text-[14px] font-medium text-white shadow-lg"
|
||||
<div
|
||||
className="fade-up absolute z-10 whitespace-nowrap rounded-lg border border-accent/30 bg-accent/12 px-3 py-1.5 text-[15px] text-accent shadow-[0_6px_18px_rgba(22,21,15,0.10)]"
|
||||
style={{
|
||||
animationDelay: "450ms",
|
||||
left: Math.min(Math.max(x + CELL / 2, 120), width - 120),
|
||||
top: above ? y - 46 : y + CELL + 14,
|
||||
x: "-50%",
|
||||
marginLeft: "-0.5px",
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
{keyLabel.charAt(0).toUpperCase() + keyLabel.slice(1)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -611,7 +611,7 @@
|
||||
},
|
||||
{
|
||||
"id": "mba",
|
||||
"text": "User did an MBA at Kellogg."
|
||||
"text": "User did an MBA a few years after college."
|
||||
},
|
||||
{
|
||||
"id": "languages",
|
||||
|
||||
+139
-137
@@ -1,6 +1,22 @@
|
||||
{
|
||||
"party_tart": {
|
||||
"recordedAt": "2026-09-19",
|
||||
"jev": {
|
||||
"ids": [
|
||||
"leo_allergy",
|
||||
"party_menu",
|
||||
"tartine_cake"
|
||||
],
|
||||
"scores": {
|
||||
"leo_allergy": 0.93,
|
||||
"party_menu": 0.77,
|
||||
"tartine_cake": 0.62
|
||||
},
|
||||
"latencyMs": 368.33912499999997,
|
||||
"costUsd": 0.00043575,
|
||||
"inputTokens": 10375,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"embeddings": {
|
||||
"ids": [
|
||||
"tartine_cake",
|
||||
@@ -9,58 +25,24 @@
|
||||
"sam_bday_nopa",
|
||||
"party_guests"
|
||||
],
|
||||
"latencyMs": 295.44437500000004,
|
||||
"latencyMs": 193.27591699999994,
|
||||
"costUsd": 0.00000221,
|
||||
"inputTokens": 17,
|
||||
"outputTokens": 0
|
||||
},
|
||||
"jev": {
|
||||
"ids": [
|
||||
"leo_allergy",
|
||||
"party_menu",
|
||||
"tartine_cake",
|
||||
"card_stolen",
|
||||
"amex"
|
||||
],
|
||||
"scores": {
|
||||
"card_stolen": 0.54,
|
||||
"leo_allergy": 0.93,
|
||||
"party_menu": 0.77,
|
||||
"tartine_cake": 0.6,
|
||||
"amex": 0.5
|
||||
},
|
||||
"latencyMs": 338.0672919999997,
|
||||
"costUsd": 0.0004356660000000001,
|
||||
"inputTokens": 10373,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"sonnet": {
|
||||
"ids": [
|
||||
"party_guests",
|
||||
"leo_allergy",
|
||||
"party_menu"
|
||||
"leo_allergy"
|
||||
],
|
||||
"latencyMs": 4706.296709,
|
||||
"costUsd": 0.017684,
|
||||
"inputTokens": 7217,
|
||||
"outputTokens": 325
|
||||
"latencyMs": 7282.527083999999,
|
||||
"costUsd": 0.019932,
|
||||
"inputTokens": 7216,
|
||||
"outputTokens": 550
|
||||
}
|
||||
},
|
||||
"dad_birthday": {
|
||||
"recordedAt": "2026-09-19",
|
||||
"embeddings": {
|
||||
"ids": [
|
||||
"dad_70",
|
||||
"offsite_dinner",
|
||||
"sam_bday_nopa",
|
||||
"dad_kokkari",
|
||||
"party_guests"
|
||||
],
|
||||
"latencyMs": 219.18404100000043,
|
||||
"costUsd": 0.0000036400000000000003,
|
||||
"inputTokens": 28,
|
||||
"outputTokens": 0
|
||||
},
|
||||
"jev": {
|
||||
"ids": [
|
||||
"dad_stent",
|
||||
@@ -73,36 +55,90 @@
|
||||
],
|
||||
"scores": {
|
||||
"dad_stent": 0.93,
|
||||
"mom_wheelchair": 0.73,
|
||||
"mom_wheelchair": 0.71,
|
||||
"sober": 0.8,
|
||||
"parents_wc": 0.65,
|
||||
"dad_kokkari": 0.9,
|
||||
"dad_kokkari": 0.89,
|
||||
"sam_bday_nopa": 0.55,
|
||||
"dad_70": 0.91
|
||||
"dad_70": 0.9
|
||||
},
|
||||
"latencyMs": 321.57887499999924,
|
||||
"costUsd": 0.00043617,
|
||||
"inputTokens": 10385,
|
||||
"latencyMs": 370.2896669999973,
|
||||
"costUsd": 0.000436254,
|
||||
"inputTokens": 10387,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"embeddings": {
|
||||
"ids": [
|
||||
"dad_70",
|
||||
"offsite_dinner",
|
||||
"sam_bday_nopa",
|
||||
"dad_kokkari",
|
||||
"party_guests"
|
||||
],
|
||||
"latencyMs": 250.02366699999766,
|
||||
"costUsd": 0.0000036400000000000003,
|
||||
"inputTokens": 28,
|
||||
"outputTokens": 0
|
||||
},
|
||||
"sonnet": {
|
||||
"ids": [
|
||||
"dad_stent",
|
||||
"mom_wheelchair",
|
||||
"dad_kokkari",
|
||||
"dad_70",
|
||||
"dad_kokkari",
|
||||
"parents_wc",
|
||||
"budget_flag",
|
||||
"sober",
|
||||
"parents_wc"
|
||||
"house_closing",
|
||||
"sober"
|
||||
],
|
||||
"latencyMs": 8792.463083000002,
|
||||
"costUsd": 0.021112,
|
||||
"inputTokens": 7236,
|
||||
"outputTokens": 664
|
||||
"latencyMs": 10199.006958999998,
|
||||
"costUsd": 0.02253,
|
||||
"inputTokens": 7235,
|
||||
"outputTokens": 806
|
||||
}
|
||||
},
|
||||
"belize_dive": {
|
||||
"recordedAt": "2026-09-19",
|
||||
"jev": {
|
||||
"ids": [
|
||||
"belize_dates",
|
||||
"seasick",
|
||||
"dive_cert",
|
||||
"sam_anniv",
|
||||
"pregnant",
|
||||
"dramamine_brand",
|
||||
"belize_blue_hole",
|
||||
"budget_flag",
|
||||
"biscuit_boarding",
|
||||
"biscuit_anxiety",
|
||||
"amex",
|
||||
"vitamins",
|
||||
"mom_wheelchair",
|
||||
"card_stolen",
|
||||
"hotel_pref"
|
||||
],
|
||||
"scores": {
|
||||
"mom_wheelchair": 0.54,
|
||||
"pregnant": 0.82,
|
||||
"biscuit_anxiety": 0.61,
|
||||
"budget_flag": 0.7,
|
||||
"card_stolen": 0.51,
|
||||
"dive_cert": 0.84,
|
||||
"belize_dates": 0.93,
|
||||
"seasick": 0.89,
|
||||
"belize_blue_hole": 0.74,
|
||||
"dramamine_brand": 0.79,
|
||||
"sam_anniv": 0.84,
|
||||
"amex": 0.61,
|
||||
"hotel_pref": 0.51,
|
||||
"biscuit_boarding": 0.67,
|
||||
"vitamins": 0.6
|
||||
},
|
||||
"latencyMs": 352.64179100000183,
|
||||
"costUsd": 0.0004356660000000001,
|
||||
"inputTokens": 10373,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"embeddings": {
|
||||
"ids": [
|
||||
"belize_dates",
|
||||
@@ -111,51 +147,11 @@
|
||||
"maya_visit",
|
||||
"travel_mexico"
|
||||
],
|
||||
"latencyMs": 180.64829199999804,
|
||||
"latencyMs": 566.6744580000013,
|
||||
"costUsd": 0.0000018200000000000002,
|
||||
"inputTokens": 14,
|
||||
"outputTokens": 0
|
||||
},
|
||||
"jev": {
|
||||
"ids": [
|
||||
"belize_dates",
|
||||
"seasick",
|
||||
"dive_cert",
|
||||
"pregnant",
|
||||
"sam_anniv",
|
||||
"dramamine_brand",
|
||||
"budget_flag",
|
||||
"belize_blue_hole",
|
||||
"biscuit_boarding",
|
||||
"amex",
|
||||
"biscuit_anxiety",
|
||||
"vitamins",
|
||||
"card_stolen",
|
||||
"hotel_pref",
|
||||
"mom_wheelchair"
|
||||
],
|
||||
"scores": {
|
||||
"mom_wheelchair": 0.53,
|
||||
"pregnant": 0.83,
|
||||
"biscuit_anxiety": 0.59,
|
||||
"budget_flag": 0.74,
|
||||
"card_stolen": 0.57,
|
||||
"dive_cert": 0.87,
|
||||
"belize_dates": 0.93,
|
||||
"seasick": 0.9,
|
||||
"belize_blue_hole": 0.72,
|
||||
"dramamine_brand": 0.8,
|
||||
"sam_anniv": 0.81,
|
||||
"amex": 0.65,
|
||||
"hotel_pref": 0.55,
|
||||
"biscuit_boarding": 0.66,
|
||||
"vitamins": 0.58
|
||||
},
|
||||
"latencyMs": 336.3340840000019,
|
||||
"costUsd": 0.00043558200000000006,
|
||||
"inputTokens": 10371,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"sonnet": {
|
||||
"ids": [
|
||||
"belize_dates",
|
||||
@@ -166,47 +162,17 @@
|
||||
"belize_blue_hole",
|
||||
"pregnant",
|
||||
"budget_flag",
|
||||
"house_closing",
|
||||
"card_stolen",
|
||||
"amex"
|
||||
],
|
||||
"latencyMs": 7694.196833000009,
|
||||
"costUsd": 0.02065,
|
||||
"inputTokens": 7215,
|
||||
"outputTokens": 622
|
||||
"latencyMs": 9425.254125000007,
|
||||
"costUsd": 0.022908,
|
||||
"inputTokens": 7214,
|
||||
"outputTokens": 848
|
||||
}
|
||||
},
|
||||
"tahoe_sunday": {
|
||||
"recordedAt": "2026-09-19",
|
||||
"jev": {
|
||||
"ids": [
|
||||
"biscuit_anxiety",
|
||||
"car_shop",
|
||||
"mom_wheelchair",
|
||||
"timezone",
|
||||
"subaru",
|
||||
"sober",
|
||||
"tahoe_cabin",
|
||||
"palisades",
|
||||
"farmers_market",
|
||||
"babysitter"
|
||||
],
|
||||
"scores": {
|
||||
"mom_wheelchair": 0.68,
|
||||
"biscuit_anxiety": 0.81,
|
||||
"car_shop": 0.72,
|
||||
"sober": 0.61,
|
||||
"palisades": 0.55,
|
||||
"tahoe_cabin": 0.61,
|
||||
"farmers_market": 0.54,
|
||||
"subaru": 0.65,
|
||||
"timezone": 0.68,
|
||||
"babysitter": 0.51
|
||||
},
|
||||
"latencyMs": 296.8981669999921,
|
||||
"costUsd": 0.000435792,
|
||||
"inputTokens": 10376,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"embeddings": {
|
||||
"ids": [
|
||||
"farmers_market",
|
||||
@@ -215,27 +181,63 @@
|
||||
"belize_dates",
|
||||
"work_travel"
|
||||
],
|
||||
"latencyMs": 365.2027080000116,
|
||||
"latencyMs": 276.0229580000014,
|
||||
"costUsd": 0.0000026,
|
||||
"inputTokens": 20,
|
||||
"outputTokens": 0
|
||||
},
|
||||
"jev": {
|
||||
"ids": [
|
||||
"biscuit_anxiety",
|
||||
"car_shop",
|
||||
"mom_wheelchair",
|
||||
"tahoe_cabin",
|
||||
"timezone",
|
||||
"sober",
|
||||
"subaru",
|
||||
"farmers_market",
|
||||
"airbnb_pref",
|
||||
"palisades",
|
||||
"babysitter",
|
||||
"budget_flag",
|
||||
"biscuit_boarding"
|
||||
],
|
||||
"scores": {
|
||||
"mom_wheelchair": 0.73,
|
||||
"biscuit_anxiety": 0.83,
|
||||
"budget_flag": 0.51,
|
||||
"car_shop": 0.78,
|
||||
"sober": 0.64,
|
||||
"palisades": 0.52,
|
||||
"tahoe_cabin": 0.67,
|
||||
"farmers_market": 0.54,
|
||||
"subaru": 0.61,
|
||||
"airbnb_pref": 0.54,
|
||||
"timezone": 0.66,
|
||||
"babysitter": 0.52,
|
||||
"biscuit_boarding": 0.51
|
||||
},
|
||||
"latencyMs": 324.19045899999037,
|
||||
"costUsd": 0.000435876,
|
||||
"inputTokens": 10378,
|
||||
"outputTokens": 4526
|
||||
},
|
||||
"sonnet": {
|
||||
"ids": [
|
||||
"car_shop",
|
||||
"car_rental",
|
||||
"budget_flag",
|
||||
"subaru",
|
||||
"biscuit_anxiety",
|
||||
"biscuit_walker",
|
||||
"biscuit_boarding",
|
||||
"budget_flag",
|
||||
"car_rental",
|
||||
"farmers_market",
|
||||
"groceries",
|
||||
"plants"
|
||||
],
|
||||
"latencyMs": 17339.568666000006,
|
||||
"costUsd": 0.030306,
|
||||
"inputTokens": 7223,
|
||||
"outputTokens": 1586
|
||||
"latencyMs": 15543.263249999989,
|
||||
"costUsd": 0.027834,
|
||||
"inputTokens": 7222,
|
||||
"outputTokens": 1339
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,20 @@ const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactCompiler: true,
|
||||
devIndicators: false,
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "Referrer-Policy", value: "no-referrer" },
|
||||
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), interest-cohort=()" },
|
||||
],
|
||||
},
|
||||
// API responses carry per-visitor key usage: never cache or store them.
|
||||
{ source: "/api/:path*", headers: [{ key: "Cache-Control", value: "no-store" }] },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user