Skip to content

Commit dd7eee1

Browse files
Implement UI/UX roadmap and ISS lesson deep links.
Unify design tokens, chat readability, landing polish, and sidebar UX; expose ISS base URL via public-config with frontend smoke tests in Python Playwright. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 20e0350 commit dd7eee1

21 files changed

Lines changed: 1298 additions & 221 deletions

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ ACL_CATALOG_PROMPT_TOP_K=5
101101
# O workflow ISS sync-kernelbot-knowledge envia o mesmo valor em KERNELBOT_RELOAD_TOKEN.
102102
ACL_RELOAD_BEARER_TOKEN=
103103

104+
# --- ISS (links de fonte no frontend) ---
105+
# Base da página de aula no ISS; o frontend monta ?d=<disciplina>&a=<slug>
106+
# KERNELBOT_ISS_PUBLIC_BASE_URL=https://gaabdevweb.github.io/ISS/public/aula.html
107+
104108
# --- Logging ---
105109
# text = linha legivel; json = uma linha JSON por evento ACL (grep/agregadores).
106110
# ACL_LOG_FORMAT=text

api/routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ async def health() -> dict[str, str]:
4646
return {"status": "ok"}
4747

4848

49+
@router.get("/api/public-config")
50+
async def public_config(request: Request) -> dict[str, str]:
51+
"""Configuração pública do frontend (URLs externas, sem segredos)."""
52+
settings = request.app.state.services.context_manager.settings
53+
return {"iss_lesson_base": settings.iss_public_lesson_base}
54+
55+
4956
@router.get("/api/curriculum")
5057
async def curriculum_all(request: Request) -> dict:
5158
"""Lista disciplinas com aulas no catálogo."""

bin/validate-frontend.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""Smoke test do frontend (Playwright Python — evita conflito npm/lightningcss)."""
3+
from __future__ import annotations
4+
5+
import json
6+
import re
7+
import sys
8+
import urllib.request
9+
10+
from playwright.sync_api import sync_playwright
11+
12+
BASE = __import__("os").environ.get("SMOKE_BASE_URL", "http://127.0.0.1:8001")
13+
14+
SKIP_INTRO_JS = """() => {
15+
try { sessionStorage.setItem('kernel_intro_seen', '1'); } catch (_) {}
16+
document.querySelectorAll('.entrance-init-hidden').forEach((el) => {
17+
el.classList.remove('entrance-init-hidden');
18+
});
19+
}"""
20+
21+
22+
def skip_entrance_animation(page) -> None:
23+
page.evaluate(SKIP_INTRO_JS)
24+
25+
26+
def wait_for_chat_input(page, timeout: float = 15000) -> None:
27+
page.locator("#message-input").wait_for(state="visible", timeout=timeout)
28+
29+
30+
def check_public_config() -> tuple[str, bool]:
31+
try:
32+
with urllib.request.urlopen(f"{BASE}/api/public-config", timeout=10) as resp:
33+
data = json.loads(resp.read().decode())
34+
ok = bool(data.get("iss_lesson_base", "").startswith("http"))
35+
return "public-config ISS base", ok
36+
except Exception as exc:
37+
return "public-config ISS base", False if not str(exc) else False
38+
39+
40+
def main() -> int:
41+
results: list[dict[str, object]] = []
42+
43+
label, ok = check_public_config()
44+
results.append({"check": label, "ok": ok})
45+
46+
with sync_playwright() as p:
47+
browser = p.chromium.launch(headless=True)
48+
page = browser.new_page()
49+
try:
50+
page.goto(BASE, wait_until="networkidle", timeout=30000)
51+
52+
page.wait_for_function(
53+
"""() => /disciplinas/i.test(
54+
document.querySelector('.entrance-title-text')?.textContent || ''
55+
)""",
56+
timeout=15000,
57+
)
58+
59+
hero = page.locator(".entrance-title-text").text_content(timeout=15000) or ""
60+
results.append({"check": "T5 hero", "ok": bool(re.search(r"disciplinas", hero, re.I))})
61+
62+
pill_count = page.locator(".entrance-discipline-pills .cmd-pill").count()
63+
results.append({"check": "T4 pills", "ok": pill_count >= 5})
64+
65+
results.append({
66+
"check": "T1 notice hidden empty",
67+
"ok": page.locator("#context-window-notice").is_hidden(),
68+
})
69+
70+
page.evaluate(
71+
"""() => {
72+
try { sessionStorage.setItem('kernel_intro_seen', '1'); } catch (_) {}
73+
const turns = [];
74+
for (let i = 0; i < 13; i++) {
75+
turns.push({ role: "user", text: `q${i}`, ts: Date.now() });
76+
turns.push({ role: "bot", text: `a${i}`, ts: Date.now() });
77+
}
78+
localStorage.setItem(
79+
"kernel_conversations_v2",
80+
JSON.stringify({
81+
activeId: "test",
82+
conversations: [{
83+
id: "test", title: "Test", createdAt: 0, updatedAt: 0,
84+
session_id: "x", turns
85+
}],
86+
}),
87+
);
88+
}"""
89+
)
90+
page.reload(wait_until="networkidle")
91+
page.locator(".message-row").first.wait_for(state="attached", timeout=15000)
92+
page.wait_for_function(
93+
"() => document.body.classList.contains('chat-active')",
94+
timeout=15000,
95+
)
96+
skip_entrance_animation(page)
97+
results.append({
98+
"check": "T1 notice with 26 turns",
99+
"ok": page.locator("#context-window-notice").is_visible(),
100+
})
101+
102+
wait_for_chat_input(page)
103+
# T3 slash menu — com histórico carregado o input está visível
104+
page.locator("#message-input").fill("/py")
105+
page.wait_for_timeout(300)
106+
results.append({
107+
"check": "T3 slash menu",
108+
"ok": page.locator("#slash-command-menu:not([hidden])").is_visible(),
109+
})
110+
111+
page.goto(f"{BASE}/?d=python", wait_until="networkidle")
112+
skip_entrance_animation(page)
113+
wait_for_chat_input(page)
114+
input_val = page.locator("#message-input").input_value()
115+
results.append({"check": "T11 ?d=python", "ok": input_val.startswith("/python")})
116+
117+
page.goto(BASE, wait_until="networkidle")
118+
results.append({
119+
"check": "T10 sidebar",
120+
"ok": page.locator("#conversation-sidebar").count() == 1,
121+
})
122+
123+
# UI/UX roadmap: public-config carregado no boot
124+
iss_base = page.evaluate(
125+
"""async () => {
126+
const r = await fetch('/api/public-config');
127+
if (!r.ok) return '';
128+
const j = await r.json();
129+
return j.iss_lesson_base || '';
130+
}"""
131+
)
132+
results.append({
133+
"check": "ISS config fetch in page",
134+
"ok": str(iss_base).startswith("http"),
135+
})
136+
finally:
137+
browser.close()
138+
139+
print(json.dumps(results, indent=2, ensure_ascii=False))
140+
failed = [r for r in results if not r["ok"]]
141+
return 1 if failed else 0
142+
143+
144+
if __name__ == "__main__":
145+
sys.exit(main())

core/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class Settings:
8686
catalog_router_prompt: str
8787
# Token Bearer para /reload e GET /health/catalog (CI, operadores).
8888
reload_bearer_token: str | None
89+
# URL pública da aula no ISS (frontend — links de fonte).
90+
iss_public_lesson_base: str
8991

9092
@property
9193
def openrouter_headers(self) -> dict[str, str]:
@@ -307,6 +309,10 @@ def _env_int(name: str, default: int, lo: int, hi: int) -> int:
307309
or None
308310
)
309311

312+
iss_public_lesson_base = (
313+
os.getenv("KERNELBOT_ISS_PUBLIC_BASE_URL") or ""
314+
).strip().rstrip("?") or "https://gaabdevweb.github.io/ISS/public/aula.html"
315+
310316
""" !Credenciais do banco! """
311317

312318
db_host = _normalize_db_host(os.getenv("DB_HOST") or "")
@@ -372,4 +378,5 @@ def _env_int(name: str, default: int, lo: int, hi: int) -> int:
372378
catalog_prompt_top_k=catalog_prompt_top_k,
373379
catalog_router_prompt=catalog_router_prompt,
374380
reload_bearer_token=reload_bearer_token,
381+
iss_public_lesson_base=iss_public_lesson_base,
375382
)

frontend/assets/css/discipline-panel.css

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737

3838
.discipline-panel__title {
3939
margin: 0;
40-
font-size: 15px;
40+
font-size: var(--font-size-sm);
4141
font-weight: 600;
4242
letter-spacing: -0.01em;
4343
}
4444

4545
.discipline-panel__close {
4646
flex-shrink: 0;
47-
font-size: 12px;
47+
font-size: var(--font-size-xs);
4848
padding: 6px 10px;
4949
border: 1px solid var(--border);
5050
border-radius: var(--radius-md);
@@ -66,13 +66,13 @@
6666

6767
.discipline-panel__progress {
6868
margin: 0 0 16px;
69-
font-size: 12px;
69+
font-size: var(--font-size-xs);
7070
color: var(--text-muted);
7171
}
7272

7373
.discipline-panel__empty {
7474
margin: 0;
75-
font-size: 13px;
75+
font-size: var(--font-size-sm);
7676
color: var(--text-muted);
7777
line-height: 1.5;
7878
}
@@ -90,7 +90,7 @@
9090
display: flex;
9191
align-items: flex-start;
9292
gap: 10px;
93-
font-size: 13px;
93+
font-size: var(--font-size-sm);
9494
line-height: 1.4;
9595
}
9696

@@ -123,7 +123,7 @@
123123
display: inline-flex;
124124
align-items: center;
125125
justify-content: center;
126-
font-size: 11px;
126+
font-size: var(--font-size-xs);
127127
font-weight: 700;
128128
}
129129

@@ -150,7 +150,7 @@
150150
border-radius: var(--radius-md);
151151
background: var(--brand-surface);
152152
color: var(--text);
153-
font-size: 13px;
153+
font-size: var(--font-size-sm);
154154
font-weight: 500;
155155
cursor: pointer;
156156
text-align: left;

0 commit comments

Comments
 (0)