Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions atlas/connectors/web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,15 @@ def rich(query: str, *, max_results: int | None = None) -> dict[str, Any]:
except Exception as exc:
rows, text = [], f"Web search failed ({type(exc).__name__})."
images = _images(query, max(n + 1, 6))
article = None
if rows:
title, body, url = rows[0]
article = {"title": title, "url": url, "summary": body,
"image": images[0]["image"] if images else None}
return {"query": query, "text": text, "images": images, "article": article}
# Numbered article results (the Owner can say "open article 2"): each real
# web result gets a thumbnail (best-effort, zipped by index) for the HUD.
results = []
for i, (title, body, url) in enumerate(rows):
img = images[i]["image"] if i < len(images) else (images[0]["image"] if images else None)
results.append({"n": i + 1, "title": title, "url": url, "summary": body, "image": img})
article = results[0] if results else None
return {"query": query, "text": text, "images": images,
"results": results, "article": article}


# ----- article reader (fetch + extract main text) ------------------------ #
Expand Down
257 changes: 233 additions & 24 deletions atlas/interface/web/app.js

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion atlas/interface/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,25 @@ <h2>Settings</h2>
</div>
</div>

<!-- search visuals: floating related photos + the top article card -->
<!-- search visuals: floating numbered photos + the top article card -->
<div class="search-visuals" id="searchVisuals" aria-hidden="true"></div>
<aside class="article-card glass" id="articleCard" hidden></aside>

<!-- in-HUD article reader: live iframe with a text-Reader fallback (never leaves ATLAS) -->
<div class="article-frame glass" id="articleFrame" hidden>
<header class="af-head">
<span class="af-num" id="afNum"></span>
<span class="af-title" id="afTitle">Article</span>
<div class="af-actions">
<button class="navbtn small" id="afLive" title="Live page">◐ Live</button>
<button class="navbtn small" id="afReader" title="Readable text">▤ Reader</button>
<button class="navbtn small" id="afTldr" title="Ask ATLAS to summarize">TLDR</button>
<button class="iconbtn" id="afClose" title="Close (Esc)">✕</button>
</div>
</header>
<div class="af-body" id="afBody"></div>
</div>

<!-- toasts -->
<div class="toasts" id="toasts" aria-live="polite"></div>

Expand Down
47 changes: 38 additions & 9 deletions atlas/interface/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -274,16 +274,45 @@ code{font-family:var(--mono);color:var(--accent);font-size:12px}
background:rgba(0,0,0,.35);border:1px solid var(--line);border-radius:8px;
color:var(--text);padding:7px 10px;font-family:var(--mono);font-size:13px;outline:none}
.tier-row select:focus,.tier-row input:focus{border-color:var(--line-bright)}
/* ---------- search visuals: floating related photos + article card ---------- */
/* ---------- search visuals: floating numbered photos + article card ---------- */
.search-visuals{position:fixed;inset:0;z-index:6;pointer-events:none;overflow:hidden}
.float-photo{position:absolute;width:118px;height:118px;object-fit:cover;border-radius:14px;
border:1px solid var(--line-bright);box-shadow:0 10px 34px rgba(0,0,0,.5),0 0 0 1px rgba(0,224,255,.15);
pointer-events:auto;cursor:pointer;opacity:0;
animation:photoIn .5s ease forwards,floaty 7s ease-in-out infinite;transition:transform .2s,box-shadow .2s}
.float-photo:hover{transform:scale(1.07);box-shadow:0 14px 44px rgba(0,0,0,.6),0 0 0 1px var(--accent);z-index:2}
@keyframes photoIn{to{opacity:.92}}
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-12px)}}
@media (max-width:900px){.float-photo{width:82px;height:82px}}
.float-item{position:absolute;width:132px;pointer-events:auto;cursor:pointer;opacity:0;
animation:flyIn .7s cubic-bezier(.2,.85,.25,1) both,floaty 7s ease-in-out infinite;transition:transform .2s}
.float-item:hover{transform:scale(1.06);z-index:3}
/* pop in the screen centre, then glide to the grid slot (--dx/--dy set per item) */
@keyframes flyIn{
0%{transform:translate(var(--dx,0),var(--dy,0)) scale(1.45);opacity:0}
14%{opacity:1}
100%{transform:translate(0,0) scale(1);opacity:1}}
.float-item img{display:block;width:132px;height:96px;object-fit:cover;border-radius:13px;
border:1px solid var(--line-bright);box-shadow:0 10px 34px rgba(0,0,0,.5),0 0 0 1px rgba(0,224,255,.15)}
.float-item:hover img{box-shadow:0 14px 44px rgba(0,0,0,.6),0 0 0 1px var(--accent)}
.float-num{position:absolute;top:-9px;left:-9px;width:24px;height:24px;border-radius:50%;
background:var(--accent);color:#04121a;font-family:var(--mono);font-weight:700;font-size:13px;
display:flex;align-items:center;justify-content:center;box-shadow:0 3px 10px rgba(0,0,0,.5);z-index:2}
.float-cap{margin-top:5px;font-size:11px;line-height:1.3;color:var(--text);
background:rgba(2,4,10,.6);border-radius:7px;padding:3px 7px;
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
@keyframes photoIn{to{opacity:1}}
@keyframes floaty{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
@media (max-width:900px){.float-item,.float-item img{width:104px}.float-item img{height:74px}}

/* in-HUD article reader (iframe + text Reader fallback) */
.article-frame{position:fixed;z-index:11;left:50%;top:50%;transform:translate(-50%,-50%);
width:min(920px,92vw);height:min(80vh,760px);display:flex;flex-direction:column;
border-radius:16px;overflow:hidden;opacity:0;animation:photoIn .3s ease forwards}
.af-head{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--line)}
.af-num{flex:0 0 auto;min-width:24px;height:24px;padding:0 6px;border-radius:12px;background:var(--accent);
color:#04121a;font-family:var(--mono);font-weight:700;font-size:13px;display:flex;align-items:center;justify-content:center}
.af-title{flex:1;min-width:0;font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.af-actions{display:flex;gap:8px;align-items:center}
.af-body{flex:1;min-height:0;position:relative;background:#0a0e16}
.af-iframe{width:100%;height:100%;border:0;background:#fff}
.af-reader{width:100%;height:100%;overflow:auto;padding:22px 30px;line-height:1.7;font-size:15px;color:var(--text)}
.af-reader h1{font-size:20px;margin:0 0 14px}
.af-reader p{margin:0 0 13px;color:#cdd6e4}
.af-note{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);font-size:11px;color:var(--muted);
background:rgba(2,4,10,.7);border:1px solid var(--line);border-radius:20px;padding:5px 12px;pointer-events:none}
.article-card{position:fixed;right:22px;bottom:22px;z-index:8;width:344px;max-width:calc(100vw - 32px);
border-radius:16px;overflow:hidden;display:flex;flex-direction:column;opacity:0;animation:photoIn .4s ease forwards}
.artc-hero{height:150px;background-size:cover;background-position:center;border-bottom:1px solid var(--line)}
Expand Down
4 changes: 3 additions & 1 deletion atlas/orchestration/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def _t_web_search(self, args: dict[str, Any]) -> str:
data = rich(args.get("query", ""))
# Stash visuals (images + top article) for the HUD; the model gets the text.
self.last_media = {"type": "search", "query": data.get("query"),
"images": data.get("images") or [], "article": data.get("article")}
"images": data.get("images") or [],
"results": data.get("results") or [],
"article": data.get("article")}
return data.get("text", "")

def _t_read_article(self, args: dict[str, Any]) -> str:
Expand Down
13 changes: 13 additions & 0 deletions atlas/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,19 @@ def tts(body: TTSIn) -> Response:
return Response(content=wav, media_type="audio/wav")


@app.get("/api/read")
def read_page(url: str) -> dict[str, Any]:
"""Extract an article's readable text so the HUD reader panel can show it
in-app when a site blocks iframe embedding. Keeps the Owner inside ATLAS."""
from ..connectors.web_search import read_article
text = read_article(url)
bad = text.startswith(("Couldn't", "There's no", "Web search"))
title, body = "", text
if not bad and "\n\n" in text:
title, body = text.split("\n\n", 1)
return {"ok": not bad, "url": url, "title": title.strip(), "text": body.strip()}


@app.get("/api/logs")
def logs(limit: int = 200) -> list[dict[str, Any]]:
"""Recent structured events for the Logs drawer (oldest→newest)."""
Expand Down
12 changes: 12 additions & 0 deletions atlas/tests/test_web_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ def test_rich_shapes_media(monkeypatch):
assert "Web results" in d["text"]


def test_rich_numbers_results_for_open_article_n(monkeypatch):
rows = [("A", "sa", "https://a"), ("B", "sb", "https://b"), ("C", "sc", "https://c")]
monkeypatch.setattr(ws, "_rows", lambda q, n, web: rows)
monkeypatch.setattr(ws, "_images", lambda q, n=6: [
{"image": f"https://img/{i}.jpg", "thumbnail": "", "source": "", "title": ""} for i in range(3)])
d = ws.rich("news")
assert [r["n"] for r in d["results"]] == [1, 2, 3] # numbered for "open article N"
assert d["results"][1]["url"] == "https://b" # article 2 → second result
assert d["results"][2]["image"] == "https://img/2.jpg" # thumbnail zipped by index
assert d["article"]["url"] == "https://a" # top result


def test_toolbox_web_search_sets_last_media(monkeypatch):
monkeypatch.setattr(ws, "rich", lambda q: {
"query": q, "text": "the text for the model",
Expand Down
Loading