diff --git a/app.py b/app.py
index 3e17a25..e3c149c 100644
--- a/app.py
+++ b/app.py
@@ -1209,9 +1209,9 @@ def synonyms_for(w: str, limit: int) -> list[dict]:
broader terms, and related words. Input is lemmatized first so
'keys' and 'feeling' resolve to 'key' and 'feel'."""
wn = get_wordnet()
- base = w
+ base = w.replace(" ", "_")
for pos in ("n", "v", "a", "r"):
- m = wn.morphy(w, pos)
+ m = wn.morphy(base, pos)
if m:
base = m
break
@@ -1298,9 +1298,13 @@ def get_homophones(w: str, phones: str) -> list[str]:
@app.get("/api/word")
def word_info(word: str):
- """Phonetic anatomy of a word: phones, syllables, stress, rime."""
- w = word.strip().lower()[:64]
- phones = phones_for(w)
+ """Phonetic anatomy of a word — or a phrase, read straight through."""
+ w = " ".join(word.strip().lower().split()[:4])[:64]
+ if " " in w:
+ parts = [phones_for(p) for p in w.split()]
+ phones = " ".join(p for p in parts if p) if all(parts) else None
+ else:
+ phones = phones_for(w)
if not phones:
return {"word": w, "known": False}
pl = phones.split()
@@ -1310,9 +1314,9 @@ def word_info(word: str):
senses = None
try:
wn = get_wordnet()
- base = w
+ base = w.replace(" ", "_")
for pos in ("n", "v", "a", "r"):
- m = wn.morphy(w, pos)
+ m = wn.morphy(base, pos)
if m:
base = m
break
@@ -1322,7 +1326,7 @@ def word_info(word: str):
return {"word": w, "known": True,
"phones": DIGITS.sub("", phones), "syl": len(stress),
"stress": stress, "rime": rime, "senses": senses,
- "homophones": get_homophones(w, phones),
+ "homophones": [] if " " in w else get_homophones(w, phones),
"zipf": round(zipf_frequency(w, "en"), 1)}
@@ -1330,6 +1334,10 @@ def word_info(word: str):
def lookup(word: str, mode: str = "rhyme", limit: int = 60):
w = word.strip().lower()[:64]
limit = min(limit, 200)
+ rhyme_on = None
+ if " " in w and mode != "syn":
+ rhyme_on = w.split()[-1] # a phrase rhymes on its final word
+ w = rhyme_on
if mode == "syn":
sections = synonyms_for(w, limit)
return {"word": w, "mode": mode, "known": bool(sections),
@@ -1346,7 +1354,8 @@ def lookup(word: str, mode: str = "rhyme", limit: int = 60):
# rhyme mode carries both: perfect in "words", slant in "near"
words = _ranked(perfect, {w}, limit)
near = _ranked(near_cands, {w}, limit // 2)
- return {"word": w, "mode": mode, "known": True, "words": words, "near": near}
+ return {"word": w, "mode": mode, "known": True, "words": words,
+ "near": near, "rhyme_on": rhyme_on}
app.mount("/", StaticFiles(directory=Path(__file__).parent / "static",
diff --git a/static/index.html b/static/index.html
index 5b95d1b..5de2ff7 100644
--- a/static/index.html
+++ b/static/index.html
@@ -187,6 +187,11 @@
.tab:hover { color: var(--ink); }
.panel-body { padding: 16px; overflow-y: auto; flex: 1; }
.lookup-row { display: flex; gap: 8px; }
+ #histBar { color: var(--ink-dim); font-size: 12px; margin: 8px 2px 0; min-height: 0; }
+ #histBar .hist { cursor: pointer; }
+ #histBar .hist:hover { color: var(--accent); }
+ .more { color: var(--accent); cursor: pointer; font-size: 11px; letter-spacing: 0; text-transform: lowercase; }
+ .res-label.sub { color: #6a5f52; margin: 8px 0 2px; }
.lookup-row input {
flex: 1; font-family: inherit; font-size: 14px;
background: var(--panel-2); border: 1px solid var(--line);
@@ -201,14 +206,8 @@
}
.seg button.active { color: var(--accent); border-color: var(--accent); }
.results { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 6px; }
- .defcard {
- background: var(--panel-2); border: 1px solid var(--line);
- border-radius: 10px; padding: 12px 14px; margin: 10px 0 4px;
- }
- .defhead { display: flex; align-items: baseline; gap: 8px; font-size: 14px; }
- .defx { margin-left: auto; cursor: pointer; color: var(--ink-dim); font-size: 15px; }
- .defx:hover { color: var(--r6); }
- .defphon { font-size: 12px; margin: 8px 0 10px; line-height: 1.6; }
+ .defhead { margin-top: 14px; font-size: 15px; }
+ .defphon { font-size: 12px; margin: 4px 0 2px; line-height: 1.6; }
.defphon i { color: var(--accent-2); font-style: normal; }
.chip {
font-size: 13px; background: var(--panel-2); border: 1px solid var(--line);
@@ -304,17 +303,12 @@ Double-click any word to look it up on the right.">
-
-
-
-
-
-
-
+
+
-
Type a word and hit Go, or double-click a word in your draft. A phonetic readout pins on top; the buttons switch rhymes, synonyms, and describing words. Tip: end a word with “?” to jump to synonyms.
+
Type a word — or double-click one in your draft — and its whole entry appears: how it sounds, what describes it, what rhymes, what could replace it.
@@ -697,9 +691,8 @@ function lookupSelection(){
const sel = editor.value.slice(editor.selectionStart, editor.selectionEnd)
.trim().replace(/[^A-Za-z']/g,'');
if(sel && !sel.includes(' ')){
- document.getElementById('lookupInput').value = sel;
tab('lookup');
- setMode('desc');
+ lookupFor(sel);
}
}
editor.addEventListener('dblclick', lookupSelection);
@@ -713,97 +706,79 @@ if(window.matchMedia('(pointer: coarse)').matches){
/* ---------- insert at cursor ---------- */
/* ============================================================
- LOOKUP — rhymes & near rhymes come from our backend (CMU dict,
- frequency-ranked). Synonyms come from the free Datamuse API.
+ WORD ENTRIES — one lookup renders the whole entry: phonetic
+ readout, describes, rhymes (+near), synonyms. Rhymes & sounds
+ come from our engine; describes comes from Datamuse.
============================================================ */
-const modeSeg = document.getElementById('modeSeg');
-let mode = 'desc';
-function setMode(m){
- mode = m;
- [...modeSeg.children].forEach(c=>c.classList.toggle('active', c.dataset.mode === m));
+const lookupInput = document.getElementById('lookupInput');
+const resultsBox = document.getElementById('lookupResults');
+const histBar = document.getElementById('histBar');
+const defBox = document.getElementById('defBox');
+let curWord = null, lookupSeq = 0, entry = null;
+const wordHistory = [];
+
+lookupInput.addEventListener('focus', ()=> lookupInput.select());
+lookupInput.addEventListener('keydown', e=>{ if(e.key === 'Enter') doLookup(); });
+lookupInput.addEventListener('input', debounce(()=>{
+ const w = lookupInput.value.trim();
+ if(w.length >= 2 && /^[A-Za-z' -]+\??$/.test(w)) doLookup();
+}, 400));
+
+function lookupFor(word){
+ lookupInput.value = word;
doLookup();
}
-modeSeg.addEventListener('click', e=>{
- const b = e.target.closest('button'); if(!b) return;
- setMode(b.dataset.mode);
-});
-document.getElementById('lookupBtn').addEventListener('click', doLookup);
-document.getElementById('lookupInput').addEventListener('keydown', e=>{ if(e.key==='Enter') doLookup(); });
-const lookupInput = document.getElementById('lookupInput');
-lookupInput.addEventListener('focus', ()=> lookupInput.select());
-const resultsBox = document.getElementById('lookupResults');
+function renderHistory(){
+ const past = wordHistory.filter(w=>w !== curWord).slice(-5).reverse();
+ histBar.innerHTML = past.length
+ ? '↩ ' + past.map(w=>`${esc(w)}`).join(' · ')
+ : '';
+ histBar.querySelectorAll('.hist').forEach(el=>
+ el.addEventListener('click', ()=> lookupFor(el.dataset.w)));
+}
+
async function doLookup(){
- let word = document.getElementById('lookupInput').value.trim();
- // RhymeZone-style: a trailing ? jumps to synonyms
- if(word.endsWith('?')){
- word = word.slice(0, -1).trim();
- document.getElementById('lookupInput').value = word;
- if(word) setMode('syn');
- return;
- }
- if(!word) return;
+ const word = lookupInput.value.trim().replace(/\?$/, '').toLowerCase();
+ if(!word || word === curWord) return;
document.getElementById('tab-lookup').scrollTop = 0;
- // the card belongs to the word, not the submode: rebuild only when
- // the word changes, and respect a dismissal until then
- if(word.toLowerCase() !== defWord){
- defWord = word.toLowerCase();
- defDismissed = false;
- showDefinition(word);
+ if(curWord){
+ const i = wordHistory.indexOf(curWord);
+ if(i >= 0) wordHistory.splice(i, 1);
+ wordHistory.push(curWord);
+ if(wordHistory.length > 8) wordHistory.shift();
}
- resultsBox.innerHTML = '
';
- }
+function chipHtml(items, cls){
+ // items: strings or {word, z}; words already in the draft get a tick
+ const inDraft = new Set((editor.value.toLowerCase().match(/[a-z']+/g)) || []);
+ return '