feat: offline in-browser "Ask MSOS" assistant (no API, zero cost)
A privacy-friendly help widget that answers visitor questions from the site's own content and links to the full page — runs 100% in the browser, no API, no server, no per-token cost. - tools/build_search_index.py: parses every EN/MK/SI page into ~720 heading-level chunks per language -> assistant/index-<lang>.json (lazy-loaded only when the chat is opened; gzips to ~80 KB). - assistant.js: dependency-free BM25 search + multilingual stopword filtering + a curated short-answer layer for common questions, plus an accessible chat widget (role=dialog, keyboard, aria). CSP-safe (self only). - assistant.css: scoped .msa-* styles in the site's teal palette; responsive (full-screen panel on mobile). - Injected on 208 pages (blog WIP excluded; picks it up when that lands). Verified in a headless browser across EN/MK/SI: curated answers, search fallback with correct ranking (rare terms win), correct deep links, Cyrillic + Slovene diacritics, and zero console errors. Curated MK/SI answers are machine-drafted — proofread before launch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cca6fd18aa
commit
f7f297bd65
214 files changed
+876
No files matched your search
@@ -0,0 +1,94 @@
|
||||
/* ==========================================================================
|
||||
MSOS Assistant widget styles. All selectors are .msa-* scoped so they can't
|
||||
collide with the site. Colours echo the site's teal accent.
|
||||
========================================================================== */
|
||||
.msa-root {
|
||||
--msa-accent: #12707e;
|
||||
--msa-accent-dark: #0d5560;
|
||||
--msa-bg: #ffffff;
|
||||
--msa-bot: #f1f5f6;
|
||||
--msa-user: #12707e;
|
||||
--msa-text: #16232a;
|
||||
position: fixed;
|
||||
right: 20px; bottom: 20px;
|
||||
z-index: 9999;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* launcher */
|
||||
.msa-launch {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: var(--msa-accent); color: #fff;
|
||||
border: 0; border-radius: 999px;
|
||||
padding: 12px 18px; font-size: 15px; font-weight: 600;
|
||||
cursor: pointer; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
||||
transition: transform .15s ease, background .15s ease;
|
||||
}
|
||||
.msa-launch:hover { background: var(--msa-accent-dark); transform: translateY(-1px); }
|
||||
.msa-launch i { font-size: 17px; }
|
||||
.msa-open .msa-launch { display: none; }
|
||||
|
||||
/* panel */
|
||||
.msa-panel {
|
||||
position: absolute; right: 0; bottom: 0;
|
||||
width: min(380px, calc(100vw - 32px));
|
||||
height: min(560px, calc(100vh - 40px));
|
||||
display: none; flex-direction: column;
|
||||
background: var(--msa-bg); border-radius: 16px;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
.msa-open .msa-panel { display: flex; }
|
||||
|
||||
.msa-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: var(--msa-accent); color: #fff;
|
||||
padding: 14px 16px; font-weight: 700; font-size: 16px;
|
||||
}
|
||||
.msa-x { background: none; border: 0; color: #fff; font-size: 26px; line-height: 1; cursor: pointer; padding: 0 4px; }
|
||||
.msa-x:hover { opacity: .8; }
|
||||
|
||||
.msa-log {
|
||||
flex: 1; overflow-y: auto; padding: 14px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
background: #fafbfb;
|
||||
}
|
||||
.msa-msg { display: flex; }
|
||||
.msa-bot { justify-content: flex-start; }
|
||||
.msa-user { justify-content: flex-end; }
|
||||
.msa-bubble {
|
||||
max-width: 85%; padding: 10px 13px; border-radius: 14px;
|
||||
font-size: 14.5px; line-height: 1.5; color: var(--msa-text);
|
||||
background: var(--msa-bot); border-bottom-left-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.msa-user .msa-bubble {
|
||||
background: var(--msa-user); color: #fff; border-radius: 14px; border-bottom-right-radius: 4px;
|
||||
}
|
||||
.msa-also { color: #5a6b72; font-size: 13px; }
|
||||
.msa-link {
|
||||
display: inline-block; margin-top: 6px;
|
||||
color: var(--msa-accent); font-weight: 600; text-decoration: none;
|
||||
}
|
||||
.msa-link:hover { text-decoration: underline; }
|
||||
|
||||
.msa-form { display: flex; gap: 8px; padding: 10px; border-top: 1px solid #eceff0; background: #fff; }
|
||||
.msa-input {
|
||||
flex: 1; border: 1px solid #d6dcde; border-radius: 10px;
|
||||
padding: 10px 12px; font-size: 14.5px; font-family: inherit; color: var(--msa-text);
|
||||
}
|
||||
.msa-input:focus { outline: 2px solid var(--msa-accent); outline-offset: -1px; border-color: var(--msa-accent); }
|
||||
.msa-send {
|
||||
background: var(--msa-accent); color: #fff; border: 0; border-radius: 10px;
|
||||
width: 44px; cursor: pointer; font-size: 16px;
|
||||
}
|
||||
.msa-send:hover { background: var(--msa-accent-dark); }
|
||||
.msa-foot { margin: 0; padding: 8px 12px; font-size: 11.5px; color: #8a979d; background: #fff; text-align: center; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.msa-root { right: 12px; bottom: 12px; }
|
||||
.msa-panel { width: calc(100vw - 24px); height: calc(100vh - 24px); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.msa-launch { transition: none; }
|
||||
}
|
||||
Reference in new issue
Block a user