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
41 changes: 22 additions & 19 deletions website/src/components/FeedbackComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export function FeedbackComponent() {
const [comment, setComment] = useState('');
const [showComment, setShowComment] = useState(false);

const handleFeedback = (type) => {
const handleFeedback = (event, type) => {
event.preventDefault();
setFeedback(type);
setShowComment(true);
};
Expand Down Expand Up @@ -34,38 +35,40 @@ export function FeedbackComponent() {
};

if (isSubmitted) {
return (
return <>
<hr style={{marginBottom: "1rem"}} />
<div className="feedback-container">
<div className="feedback-success">
<span className="feedback-success-icon">✓</span>
<span>Thanks for your feedback!</span>
</div>
</div>
);
</>
}

return (
return <>
<hr style={{marginBottom: "1rem"}} />
<div className="feedback-container">
{!showComment ? (
<div className="feedback-question">
<span className="feedback-text">Was this page helpful?</span>
<div className="feedback-buttons">
<button
className="feedback-btn feedback-btn-yes"
onClick={() => handleFeedback('yes')}
title="Yes, this was helpful"
<a
className="feedback-btn"
href="#"
onClick={(event) => handleFeedback(event, 'yes')}
// title="Yes, this was helpful"
>
<span className="feedback-emoji">👍</span>
<span>Yes</span>
</button>
<button
className="feedback-btn feedback-btn-no"
onClick={() => handleFeedback('no')}
title="No, this wasn't helpful"
👍 Yes
</a>
<a
className="feedback-btn"
href="#"
onClick={(event) => handleFeedback(event, 'no')}
// title="No, this wasn't helpful"
>
<span className="feedback-emoji">👎</span>
<span>No</span>
</button>
👎 No
</a>
</div>
</div>
) : (
Expand Down Expand Up @@ -93,5 +96,5 @@ export function FeedbackComponent() {
</div>
)}
</div>
);
</>;
}
2 changes: 2 additions & 0 deletions website/src/theme/DocItem/Layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import styles from './styles.module.css';


import { FeedbackComponent } from '../../../components/FeedbackComponent';
import ChatbotTrigger from '../../SearchBar/components/ChatbotTrigger';

/**
* Decide if the toc should be rendered, on mobile or desktop viewports
Expand Down Expand Up @@ -57,6 +58,7 @@ export default function DocItemLayout({ children }) {
<DocItemContent>{children}</DocItemContent>
</article>
{!isMain && <>
<ChatbotTrigger />
<FeedbackComponent />
<DocItemPaginator />
<DocItemFooter />
Expand Down
18 changes: 16 additions & 2 deletions website/src/theme/SearchBar/components/AIChatInSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ interface AIChatInSearchProps {
onSaveConversation?: (data: SavedConversation) => void;
onClearConversation?: () => void;
savedConversation?: SavedConversation | null;
initialMessage?: string | null;
}

function getDisplayText(text: string): string {
return text.replace(/\n\n\[Page:[^\]]*\]$/, '').trim();
}

const SUGGESTIONS = [
Expand All @@ -29,6 +34,7 @@ export default function AIChatInSearch({
onSaveConversation,
onClearConversation,
savedConversation,
initialMessage,
}: AIChatInSearchProps) {
const { colorMode } = useColorMode();

Expand All @@ -42,6 +48,14 @@ export default function AIChatInSearch({

const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const initialMessageSentRef = useRef(false);

useEffect(() => {
if (initialMessage && !initialMessageSentRef.current) {
initialMessageSentRef.current = true;
sendMessage(initialMessage);
}
}, [initialMessage, sendMessage]);

const prevIsLoadingRef = useRef(false);
useEffect(() => {
Expand Down Expand Up @@ -113,14 +127,14 @@ export default function AIChatInSearch({
{index > 0 && <hr className={styles.aiChatDelimiter} />}

<div className={styles.aiChatRow}>
<p className={styles.aiChatUserQuery}>{turn.userText}</p>
<p className={styles.aiChatUserQuery}>{getDisplayText(turn.userText)}</p>
</div>

<hr className={styles.aiChatDelimiter} />

<div className={styles.aiChatRow}>
<div className={styles.aiChatRowContent}>
{turn.assistantText?.trim() ? (
{turn.assistantText? (
<div className={styles.aiChatAnswer}>
<MarkdownRenderer part={turn.assistantText} isDarkTheme={colorMode === 'dark'} />
</div>
Expand Down
99 changes: 99 additions & 0 deletions website/src/theme/SearchBar/components/ChatbotTrigger.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
};

if (windowSize === 'mobile') {
return null;
}

return <>
<hr />
<div
ref={containerRef}
className={`${styles.chatbotTrigger} ${isStuck ? styles.chatbotTriggerStuck : ''}`}
>
<textarea
ref={textareaRef}
className={styles.chatbotTriggerTextarea}
placeholder="Talk with this document..."
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
rows={2}
aria-label="Talk with this document"
/>
<div className={styles.chatbotTriggerActions}>
<button
type="button"
className={`${styles.chatbotTriggerIconBtn} ${styles.chatbotTriggerIconBtnGreen}`}
onClick={submit}
disabled={!value.trim()}
aria-label="Send message"
title="Send message"
>
<ArrowUp size={15} />
</button>
</div>
</div>
</>;
}
4 changes: 3 additions & 1 deletion website/src/theme/SearchBar/components/useStreamingChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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) {
Expand Down
20 changes: 17 additions & 3 deletions website/src/theme/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default function SearchBar(): JSX.Element {
const [client, setClient] = useState<MeiliSearch | null>(null);
const [mode, setMode] = useState<'search' | 'askDocs'>('search');
const [savedConversation, setSavedConversation] = useState<SavedConversation | null>(null);
const [pendingMessage, setPendingMessage] = useState<string | null>(null);

const inputRef = useRef<HTMLInputElement>(null);
const resultsRef = useRef<HTMLDivElement>(null);
Expand All @@ -85,6 +86,7 @@ export default function SearchBar(): JSX.Element {
setIsOpen(false);
setQuery('');
setMode('search');
setPendingMessage(null);
}, []);

useEffect(() => {
Expand All @@ -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<OpenSearchEventDetail>;
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(() => {
Expand Down Expand Up @@ -251,7 +264,7 @@ export default function SearchBar(): JSX.Element {
</div>

{mode === 'search' && (
<>
<div className={styles.searchContent}>
<div className={styles.results} ref={resultsRef}>
{results.length === 0 && query && !loading && (
<div className={styles.noResults}>
Expand Down Expand Up @@ -300,14 +313,15 @@ export default function SearchBar(): JSX.Element {
))}
</div>
</div>
</>
</div>
)}

{mode === 'askDocs' && (
<AIChatInSearch
savedConversation={savedConversation}
onSaveConversation={setSavedConversation}
onClearConversation={() => setSavedConversation(null)}
initialMessage={pendingMessage}
/>
)}
</div>
Expand Down
Loading