- {turn.assistantText?.trim() ? (
+ {turn.assistantText? (
diff --git a/website/src/theme/SearchBar/components/ChatbotTrigger.tsx b/website/src/theme/SearchBar/components/ChatbotTrigger.tsx
new file mode 100644
index 00000000000..420e230de41
--- /dev/null
+++ b/website/src/theme/SearchBar/components/ChatbotTrigger.tsx
@@ -0,0 +1,99 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { ArrowUp } from 'lucide-react';
+import { useLocation } from '@docusaurus/router';
+import { useWindowSize } from '@docusaurus/theme-common';
+import styles from '../styles.module.css';
+
+export default function ChatbotTrigger() {
+ const [value, setValue] = useState('');
+ const [isStuck, setIsStuck] = useState(false);
+ const containerRef = useRef
(null);
+ const textareaRef = useRef(null);
+ const windowSize = useWindowSize();
+ const { pathname } = useLocation();
+
+ useEffect(() => {
+ if (windowSize === 'mobile') {
+ setIsStuck(false);
+ return;
+ }
+
+ const updateStuckState = () => {
+ const element = containerRef.current;
+ if (!element) return;
+
+ const rect = element.getBoundingClientRect();
+ const stickyBottom = Number.parseFloat(getComputedStyle(element).bottom) || 0;
+ const viewportBottom = window.innerHeight;
+ const targetBottom = viewportBottom - stickyBottom;
+
+ setIsStuck(Math.abs(rect.bottom - targetBottom) < 1.5);
+ };
+
+ updateStuckState();
+ window.addEventListener('scroll', updateStuckState, { passive: true });
+ window.addEventListener('resize', updateStuckState);
+
+ return () => {
+ window.removeEventListener('scroll', updateStuckState);
+ window.removeEventListener('resize', updateStuckState);
+ };
+ }, [windowSize]);
+
+ const getPageMdUrl = () => {
+ const clean = pathname.replace(/\/$/, '');
+ return `${clean}.md`;
+ };
+
+ const submit = () => {
+ const text = value.trim();
+ if (!text) return;
+ setValue('');
+ const message = `${text}\n\n[Page: ${getPageMdUrl()}]`;
+ console.log(message);
+
+ window.dispatchEvent(new CustomEvent('open-chatbot', { detail: { message } }));
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ submit();
+ }
+ };
+
+ if (windowSize === 'mobile') {
+ return null;
+ }
+
+ return <>
+
+
+ >;
+}
diff --git a/website/src/theme/SearchBar/components/useStreamingChat.ts b/website/src/theme/SearchBar/components/useStreamingChat.ts
index 809524f517f..3c2773391a9 100644
--- a/website/src/theme/SearchBar/components/useStreamingChat.ts
+++ b/website/src/theme/SearchBar/components/useStreamingChat.ts
@@ -21,6 +21,8 @@ interface UseStreamingChatOptions {
}
const API_URL = 'https://docs-mcp-f18b.onrender.com/api/chat';
+// const API_URL = 'http://localhost:3001/api/chat';
+// const API_URL = 'http://localhost:3001/api/chat/mock';
const metadataSchema = z.object({
sources: z.array(z.object({ title: z.string(), path: z.string() })).optional(),
@@ -30,7 +32,7 @@ export function extractMessageText(message: ChatUIMessage): string {
return message.parts
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
.map((p) => p.text)
- .join('');
+ .join('\n\n');
}
export function useStreamingChat({ savedConversation, onSaveConversation }: UseStreamingChatOptions) {
diff --git a/website/src/theme/SearchBar/index.tsx b/website/src/theme/SearchBar/index.tsx
index dad829da42e..e65d4e2eea8 100644
--- a/website/src/theme/SearchBar/index.tsx
+++ b/website/src/theme/SearchBar/index.tsx
@@ -70,6 +70,7 @@ export default function SearchBar(): JSX.Element {
const [client, setClient] = useState(null);
const [mode, setMode] = useState<'search' | 'askDocs'>('search');
const [savedConversation, setSavedConversation] = useState(null);
+ const [pendingMessage, setPendingMessage] = useState(null);
const inputRef = useRef(null);
const resultsRef = useRef(null);
@@ -85,6 +86,7 @@ export default function SearchBar(): JSX.Element {
setIsOpen(false);
setQuery('');
setMode('search');
+ setPendingMessage(null);
}, []);
useEffect(() => {
@@ -101,14 +103,25 @@ export default function SearchBar(): JSX.Element {
}, [closeModal]);
useEffect(() => {
+ const handleOpenChatbot = (e: Event) => {
+ const { message } = (e as CustomEvent<{ message: string }>).detail;
+ setPendingMessage(message);
+ setMode('askDocs');
+ setIsOpen(true);
+ };
+
const handleOpenSearch = (event: Event) => {
const customEvent = event as CustomEvent;
setMode(customEvent.detail?.mode ?? 'search');
setIsOpen(true);
};
+ window.addEventListener('open-chatbot', handleOpenChatbot);
window.addEventListener('near-docs:open-search', handleOpenSearch);
- return () => window.removeEventListener('near-docs:open-search', handleOpenSearch);
+ return () => {
+ window.removeEventListener('open-chatbot', handleOpenChatbot);
+ window.removeEventListener('near-docs:open-search', handleOpenSearch);
+ };
}, []);
useEffect(() => {
@@ -251,7 +264,7 @@ export default function SearchBar(): JSX.Element {
{mode === 'search' && (
- <>
+
{results.length === 0 && query && !loading && (
@@ -300,7 +313,7 @@ export default function SearchBar(): JSX.Element {
))}
- >
+
)}
{mode === 'askDocs' && (
@@ -308,6 +321,7 @@ export default function SearchBar(): JSX.Element {
savedConversation={savedConversation}
onSaveConversation={setSavedConversation}
onClearConversation={() => setSavedConversation(null)}
+ initialMessage={pendingMessage}
/>
)}
diff --git a/website/src/theme/SearchBar/styles.module.css b/website/src/theme/SearchBar/styles.module.css
index 213916803d9..da3f8bcd183 100644
--- a/website/src/theme/SearchBar/styles.module.css
+++ b/website/src/theme/SearchBar/styles.module.css
@@ -53,7 +53,7 @@
.dialogOverlay {
position: fixed;
inset: 0;
- z-index: 1000;
+ z-index: 9999;
display: flex;
align-items: flex-start;
justify-content: center;
@@ -252,6 +252,11 @@
transform: translateY(-1px);
}
+/* Search content wrapper */
+.searchContent {
+ display: contents;
+}
+
/* Results */
.results {
max-height: 400px;
@@ -681,6 +686,114 @@
transform: none;
}
+/* Mobile responsive */
+@media (max-width: 640px) {
+ .dialogOverlay {
+ align-items: stretch;
+ padding-top: 0;
+ }
+
+ .modal {
+ margin: 0;
+ border-radius: 0;
+ max-width: 100%;
+ height: 100vh;
+ height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ /* Header: fila 1 = input + X, fila 2 = toggle centrado */
+ .searchHeader {
+ flex-wrap: wrap;
+ padding: 0.875rem 1rem 0;
+ gap: 0;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ }
+
+ .searchHeaderContent {
+ order: 1;
+ flex: 1;
+ min-height: 40px;
+ }
+
+ .closeButton {
+ order: 2;
+ }
+
+ .modeToggle {
+ order: 3;
+ width: 100%;
+ justify-content: center;
+ padding: 0.625rem 0 0.875rem;
+ background: transparent;
+ }
+
+ .modeToggleBtn {
+ flex: 1;
+ justify-content: center;
+ max-width: 160px;
+ }
+
+ /* Search content wrapper ocupa espacio restante */
+ .searchContent {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ /* Results toma todo el espacio disponible */
+ .results {
+ max-height: unset;
+ flex: 1;
+ min-height: 0;
+ }
+
+ /* Footer (categorías) siempre abajo */
+ .footer {
+ flex-shrink: 0;
+ }
+
+ /* Ocultar scrollbar en categorías */
+ .categoryFilters {
+ scrollbar-width: none;
+ }
+
+ .categoryFilters::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* Sugerencias centradas verticalmente */
+ .aiChatMessages {
+ flex: 1;
+ min-height: 0;
+ justify-content: center;
+ }
+
+ .aiChatSuggestions {
+ padding: 1.5rem 1rem;
+ }
+
+ .aiChatSuggestionChip {
+ width: 100%;
+ text-align: left;
+ border-radius: 12px;
+ }
+
+ .aiChatContainer {
+ height: unset;
+ flex: 1;
+ min-height: 0;
+ }
+
+ .chatbotTrigger {
+ margin: 0 auto;
+ }
+}
+
/* Dark mode - AI Chat */
[data-theme='dark'] .modeToggleBtnActive {
background: var(--ifm-color-emphasis-300);
@@ -803,3 +916,80 @@
[data-theme='dark'] .aiChatSourceLink:hover {
background: rgba(0, 204, 163, 0.15) !important;
}
+
+.chatbotTrigger {
+ position: sticky;
+ bottom: 2rem;
+ display: flex;
+ padding: .5rem .8rem;
+ margin: 0 auto;
+ border-radius: 8px;
+ background-color: var(--ifm-background-surface-color);
+ border: 1px solid var(--ifm-color-emphasis-200);
+ width: 100%;
+ max-width: 100%;
+ transition: max-width 0.2s ease;
+}
+
+.chatbotTriggerStuck {
+ max-width: 60%;
+}
+
+.chatbotTriggerTextarea {
+ flex: 1;
+ border: none;
+ outline: none;
+ backdrop-filter: blur(4px);
+ background: transparent;
+ resize: none;
+ font-size: 0.9375rem;
+ font-family: inherit;
+ line-height: 1.5;
+ padding: 0;
+ margin: 0;
+ max-height: 1.5rem;
+}
+
+.chatbotTriggerTextarea::placeholder {
+ color: var(--ifm-color-emphasis-500);
+}
+
+[data-theme='light'] .chatbotTriggerTextarea::placeholder {
+ color: var(--ifm-color-emphasis-700);
+ opacity: 1;
+}
+
+.chatbotTriggerActions {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ gap: 0.375rem;
+}
+
+.chatbotTriggerIconBtn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ color: white;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+.chatbotTriggerIconBtn:hover:not(:disabled) {
+ transform: scale(1.08);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.chatbotTriggerIconBtn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.chatbotTriggerIconBtnGreen {
+ background: var(--ifm-color-primary);
+}
diff --git a/website/static/css/custom.scss b/website/static/css/custom.scss
index ea80938e562..a77e7061c4e 100644
--- a/website/static/css/custom.scss
+++ b/website/static/css/custom.scss
@@ -661,10 +661,7 @@ video+p>em,img+p>em,img+em, .monaco+em {
// ============================================================================
.feedback-container {
- margin: 2rem 0.8em 2rem 1.75rem;
- padding: 1rem;
- border: 1px solid var(--ifm-color-emphasis-300);
- border-radius: 8px;
+ padding: 0 1rem 1rem 1rem;
font-size: 0.9rem;
}
@@ -681,14 +678,14 @@ video+p>em,img+p>em,img+em, .monaco+em {
}
.feedback-btn {
- padding: 0.375rem 0.75rem;
+ padding: 0.1rem 0.75rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
- background-color: var(--ifm-background-surface-color);
color: var(--ifm-color-content);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
+ text-decoration: none;
}
.feedback-btn:hover {