diff --git a/src/_layouts/base.html b/src/_layouts/base.html
index 02c2299..2ce9e74 100644
--- a/src/_layouts/base.html
+++ b/src/_layouts/base.html
@@ -30,9 +30,43 @@
-
+
+
+
+
+ Wizz AI assistent
+
+
+ 👋 Vraag iets over de site, widgets of nieuwste updates.
+
+
+
+
+
+
{% if page.js %} {% for src in page.js %}
diff --git a/src/assets/css/main.css b/src/assets/css/main.css
index 0442dd6..9260169 100644
--- a/src/assets/css/main.css
+++ b/src/assets/css/main.css
@@ -86,23 +86,128 @@ body::before {
z-index: 0;
}
-.ai_helper_overlay {
+.ai-helper {
position: fixed;
top: 12px;
right: 12px;
+ z-index: 9999;
+}
+
+.ai-helper__toggle {
width: 64px;
height: 64px;
- z-index: 9999;
- pointer-events: none;
+ border: 0;
+ border-radius: 50%;
+ cursor: pointer;
+ background: transparent;
+ padding: 0;
+}
+
+.ai-helper__icon {
+ display: block;
+ width: 100%;
+ height: 100%;
border-radius: 50%;
overflow: hidden;
- background-image: url("data:image/svg+xml;utf8,");
background-image: url('/assets/img/g-wizz.svg');
background-size: contain;
background-repeat: no-repeat;
- animation: ai_helper_overlay 0.4s infinite alternate ease-in-out;
+ animation: ai-helper-icon 0.4s infinite alternate ease-in-out;
+}
+
+.ai-helper__chat {
+ position: absolute;
+ top: 74px;
+ right: 0;
+ width: min(90vw, 340px);
+ max-height: min(70vh, 480px);
+ background: color-mix(in oklab, var(--card) 94%, #000 6%);
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius);
+ padding: 14px;
+ box-shadow:
+ 0 0 22px rgba(0, 0, 0, 0.7),
+ var(--glow);
+ display: grid;
+ gap: 10px;
+}
+
+.ai-helper__title {
+ margin: 0;
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.ai-helper__messages {
+ display: grid;
+ gap: 8px;
+ overflow-y: auto;
+ max-height: 260px;
+ padding-right: 4px;
+}
+
+.ai-helper__message {
+ margin: 0;
+ padding: 8px 10px;
+ border: 1px solid var(--border-strong);
+ border-radius: 10px;
+ font-size: 10px;
+ line-height: 1.6;
+}
+
+.ai-helper__message--assistant {
+ background: color-mix(in oklab, var(--primary) 12%, transparent);
+}
+
+.ai-helper__message--user {
+ background: color-mix(in oklab, var(--primary) 24%, transparent);
+}
+
+.ai-helper__form {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 8px;
+}
+
+.ai-helper__input,
+.ai-helper__send {
+ font: inherit;
+ color: var(--text);
+ border: 1px solid var(--border-strong);
+ border-radius: 10px;
+ background: color-mix(in oklab, var(--card) 86%, #000 14%);
+}
+
+.ai-helper__input {
+ min-width: 0;
+ padding: 8px 10px;
+}
+
+.ai-helper__send {
+ padding: 8px 10px;
+ cursor: pointer;
+}
+
+.ai-helper__toggle:focus-visible,
+.ai-helper__input:focus-visible,
+.ai-helper__send:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 4px var(--ring);
}
-@keyframes ai_helper_overlay {
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+@keyframes ai-helper-icon {
0% {
transform: rotate(-4deg);
}
diff --git a/src/assets/js/ai-assistant.js b/src/assets/js/ai-assistant.js
new file mode 100644
index 0000000..9dadbc9
--- /dev/null
+++ b/src/assets/js/ai-assistant.js
@@ -0,0 +1,93 @@
+const DEFAULT_REPLY =
+ 'Ik heb nog geen live AI-backend. Gebruik dit venster voor snelle navigatiehulp en ideetjes.';
+
+const KEYWORD_RESPONSES = [
+ {
+ terms: ['blog', 'post', 'artikel'],
+ reply:
+ 'Open /blog/ voor nieuwste artikelen. Wil je filters? Gebruik categorieknoppen op de blogpagina.',
+ },
+ {
+ terms: ['widget', 'dashboard', 'pomodoro', 'weer', 'clock'],
+ reply: 'Dashboard bevat Pomodoro, Weather en Clock widgets. Je vindt deze via /dashboard/.',
+ },
+ {
+ terms: ['update', 'nieuw', 'changelog'],
+ reply: 'Open /updates/ om recente commits, bijdragers en release-notes te bekijken.',
+ },
+];
+
+export const normalizePrompt = (value = '') => value.trim().toLowerCase();
+
+export const getAssistantReply = (prompt) => {
+ const normalizedPrompt = normalizePrompt(prompt);
+ if (!normalizedPrompt) {
+ return 'Typ een korte vraag, bijvoorbeeld: "Waar staat de Pomodoro?"';
+ }
+
+ const match = KEYWORD_RESPONSES.find(({ terms }) =>
+ terms.some((term) => normalizedPrompt.includes(term)),
+ );
+
+ return match ? match.reply : DEFAULT_REPLY;
+};
+
+export const createMessage = (text, tone) => {
+ const message = document.createElement('p');
+ message.className = `ai-helper__message ai-helper__message--${tone}`;
+ message.textContent = text;
+ return message;
+};
+
+export const initAiAssistant = (root = document) => {
+ const toggle = root.querySelector('[data-ai-helper-toggle]');
+ const panel = root.querySelector('[data-ai-helper-chat]');
+ const form = root.querySelector('[data-ai-helper-form]');
+ const input = root.querySelector('#ai-helper-input');
+ const log = root.querySelector('[data-ai-helper-messages]');
+
+ if (!toggle || !panel || !form || !input || !log) return;
+
+ const setOpen = (isOpen) => {
+ panel.hidden = !isOpen;
+ toggle.setAttribute('aria-expanded', String(isOpen));
+ if (isOpen) {
+ input.focus();
+ }
+ };
+
+ toggle.addEventListener('click', () => {
+ const isOpen = toggle.getAttribute('aria-expanded') !== 'true';
+ setOpen(isOpen);
+ });
+
+ form.addEventListener('submit', (event) => {
+ event.preventDefault();
+
+ const prompt = input.value;
+ const cleanedPrompt = prompt.trim();
+ if (!cleanedPrompt) {
+ log.append(createMessage(getAssistantReply(''), 'assistant'));
+ log.scrollTop = log.scrollHeight;
+ form.reset();
+ input.focus();
+ return;
+ }
+
+ log.append(createMessage(cleanedPrompt, 'user'));
+ log.append(createMessage(getAssistantReply(cleanedPrompt), 'assistant'));
+ log.scrollTop = log.scrollHeight;
+ form.reset();
+ input.focus();
+ });
+
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') {
+ setOpen(false);
+ }
+ });
+};
+
+if (typeof document !== 'undefined') {
+ initAiAssistant();
+}
diff --git a/src/test/ai-assistant.test.js b/src/test/ai-assistant.test.js
new file mode 100644
index 0000000..9700b86
--- /dev/null
+++ b/src/test/ai-assistant.test.js
@@ -0,0 +1,47 @@
+import { describe, expect, it, vi } from 'vitest';
+import { getAssistantReply, normalizePrompt } from '../assets/js/ai-assistant.js';
+
+describe('ai assistant helpers', () => {
+ it('normalizes prompt text', () => {
+ expect(normalizePrompt(' HeLLo ')).toBe('hello');
+ });
+
+ it('returns guidance for empty prompts', () => {
+ expect(getAssistantReply('')).toContain('Typ een korte vraag');
+ });
+
+ it('matches prompts to known sections', () => {
+ expect(getAssistantReply('Waar is de blog pagina?')).toContain('/blog/');
+ expect(getAssistantReply('toont dashboard widgets')).toContain('/dashboard/');
+ expect(getAssistantReply('laat updates zien')).toContain('/updates/');
+ });
+
+ it('returns default fallback when no keyword matches', () => {
+ expect(getAssistantReply('onbekende vraag')).toContain('nog geen live AI-backend');
+ });
+
+ it('registers behavior only when required nodes exist', async () => {
+ vi.resetModules();
+
+ const fakeToggle = {
+ state: 'false',
+ setAttribute: vi.fn(function setAttribute(_name, value) {
+ this.state = value;
+ }),
+ getAttribute: vi.fn(function getAttribute() {
+ return this.state;
+ }),
+ addEventListener: vi.fn(),
+ };
+
+ const fakeRoot = {
+ querySelector: (selector) => {
+ if (selector === '[data-ai-helper-toggle]') return fakeToggle;
+ return null;
+ },
+ };
+
+ const { initAiAssistant } = await import('../assets/js/ai-assistant.js');
+ expect(() => initAiAssistant(fakeRoot)).not.toThrow();
+ });
+});