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
87 changes: 87 additions & 0 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,91 @@
--docusaurus-highlighted-code-line-bg: rgba(231, 111, 0, 0.2); /* Slightly more visible Orange for code highlights */
}

/* AI Follow-up Suggestion Chips
* Evidence-backed design:
* - Pill shape: industry standard (ChatGPT, Google AI) per NNG research
* - 3-4 max: Hick's Law + Miller's Law reduce cognitive load
* - Outlined, low visual weight: answer content stays dominant
* - DocSearch CSS variables: automatic dark mode support
*/
.askai-suggestions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 0;
margin-top: 8px;
}

.askai-suggestion-btn {
appearance: none;
background: var(--docsearch-hit-background, #fff);
border: 1px solid var(--docsearch-muted-color, #969faf);
border-radius: 16px;
color: var(--docsearch-text-color, #1c1e21);
cursor: pointer;
font-family: inherit;
font-size: 13px;
line-height: 1.4;
min-height: 36px;
padding: 6px 14px;
transition: background 150ms ease, border-color 150ms ease;
}

.askai-suggestion-btn:hover {
background: var(--docsearch-hit-highlight-color, rgba(0, 61, 255, 0.1));
border-color: var(--docsearch-primary-color, #003dff);
}

.askai-suggestion-btn:focus-visible {
outline: 2px solid var(--docsearch-primary-color, #003dff);
outline-offset: 2px;
}

.askai-suggestion-btn:active {
background: var(--docsearch-hit-highlight-color, rgba(0, 61, 255, 0.15));
}

/* Staggered entrance animation (NNG: appear after response completes) */
@media (prefers-reduced-motion: no-preference) {
.askai-suggestion-btn {
animation: askai-chip-enter 300ms ease forwards;
opacity: 0;
transform: translateY(8px);
}

.askai-suggestion-btn:nth-child(2) { animation-delay: 75ms; }
.askai-suggestion-btn:nth-child(3) { animation-delay: 150ms; }
.askai-suggestion-btn:nth-child(4) { animation-delay: 225ms; }

@keyframes askai-chip-enter {
to {
opacity: 1;
transform: translateY(0);
}
}
}

/* Mobile: stack vertically with WCAG 2.5.5 touch targets (44px min) */
@media (max-width: 640px) {
.askai-suggestions {
flex-direction: column;
}

.askai-suggestion-btn {
width: 100%;
text-align: left;
min-height: 44px;
}
}

/* Screen reader only text */
.askai-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
163 changes: 158 additions & 5 deletions src/theme/SearchBar/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,167 @@
import {useEffect} from 'react';
import SearchBar from '@theme-original/SearchBar';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

// Wrap SearchBar to inject agentStudio: true into askAi config.
// Docusaurus 3.9.2's Joi validation doesn't recognize agentStudio yet,
// but @docsearch/react 4.6+ requires it for Agent Studio assistants.
// Max suggestions to show (Hick's Law: fewer choices = faster decisions)
const MAX_SUGGESTIONS = 4;
const ALLOWED_PARTS = new Set(['step-start', 'text']);

function injectSuggestionButtons(suggestions) {
document.querySelectorAll('.askai-suggestions').forEach((el) => el.remove());

const responseArea = document.querySelector(
'.DocSearch-AskAiScreen-Response-Container'
);
if (!responseArea || !suggestions.length) return;

const limited = suggestions.slice(0, MAX_SUGGESTIONS);

// Accessible wrapper: role="group" + aria-live for screen readers
const wrapper = document.createElement('div');
wrapper.className = 'askai-suggestions';
wrapper.setAttribute('role', 'group');
wrapper.setAttribute('aria-label', 'Follow-up suggestions');
wrapper.setAttribute('aria-live', 'polite');

// Screen reader announcement
const srAnnounce = document.createElement('span');
srAnnounce.className = 'askai-sr-only';
srAnnounce.textContent = `${limited.length} follow-up suggestions available`;
wrapper.appendChild(srAnnounce);

limited.forEach((text) => {
const btn = document.createElement('button');
btn.className = 'askai-suggestion-btn';
btn.textContent = text;
btn.type = 'button';
btn.onclick = () => {
const input = document.querySelector(
'.DocSearch-Input, input[placeholder*="Ask"]'
);
if (input) {
const nativeSetter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value'
).set;
nativeSetter.call(input, text);
input.dispatchEvent(new Event('input', {bubbles: true}));
input.focus();
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
bubbles: true,
})
);
}
};
wrapper.appendChild(btn);
});

const containers = document.querySelectorAll(
'.DocSearch-AskAiScreen-Response-Container'
);
const last = containers[containers.length - 1] || responseArea;
last.after(wrapper);
}

function extractSuggestionsFromStream(response) {
if (!response.ok || !response.body) return;
const clone = response.clone();
const reader = clone.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

function pump() {
reader.read().then(({done, value}) => {
if (done) return;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (
data.type === 'data-suggestions' &&
data.data?.suggestions
) {
setTimeout(
() => injectSuggestionButtons(data.data.suggestions),
500
);
}
} catch {
// ignore
}
}
}
pump();
});
}
pump();
}

function useAgentStudioFetchPatch(appId) {
useEffect(() => {
if (!appId) return;
const endpoint = `https://${appId}.algolia.net/agent-studio/1`;
const originalFetch = window.fetch;

window.fetch = function (url, options) {
if (
typeof url === 'string' &&
url.startsWith(endpoint) &&
options?.method === 'POST' &&
options?.body
) {
try {
const body = JSON.parse(options.body);

// Fix assistant message parts for follow-ups
if (Array.isArray(body.messages) && body.messages.length > 1) {
body.messages = body.messages.map((msg) => {
if (msg.role === 'assistant' && Array.isArray(msg.parts)) {
const cleaned = msg.parts.filter((p) =>
ALLOWED_PARTS.has(p.type)
);
if (!cleaned.some((p) => p.type === 'step-start')) {
cleaned.unshift({type: 'step-start'});
}
return {...msg, parts: cleaned};
}
return msg;
});
}

const result = originalFetch.call(this, url, {
...options,
body: JSON.stringify(body),
});

result.then(extractSuggestionsFromStream);

return result;
} catch {
// pass through
}
}
return originalFetch.apply(this, arguments);
};

return () => {
window.fetch = originalFetch;
};
}, [appId]);
}

export default function SearchBarWrapper(props) {
const {siteConfig} = useDocusaurusContext();
const askAi = siteConfig.themeConfig.algolia?.askAi;
if (askAi && !askAi.agentStudio) {
askAi.agentStudio = true;
if (askAi) {
if (!askAi.agentStudio) askAi.agentStudio = true;
}
useAgentStudioFetchPatch(siteConfig.themeConfig.algolia?.appId);
return <SearchBar {...props} />;
}
Loading