diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 42262e08..5982f6f6 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -1,11 +1,20 @@ -import React from 'react' -import styled, { ThemeProvider } from 'styled-components'; +import React, { useRef } from 'react'; +import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { WidgetCore } from './DocsGPTWidget'; import { SearchBarProps } from '@/types'; -import { getSearchResults } from '../requests/searchAPI' +import { getSearchResults } from '../requests/searchAPI'; import { Result } from '@/types'; import MarkdownIt from 'markdown-it'; -import { getOS, preprocessSearchResultsToHTML } from '../utils/helper' +import { getOS, processMarkdownString } from '../utils/helper'; +import DOMPurify from 'dompurify'; +import { + CodeIcon, + TextAlignLeftIcon, + HeadingIcon, + ReaderIcon, + ListBulletIcon, + QuoteIcon +} from '@radix-ui/react-icons'; const themes = { dark: { bg: '#000', @@ -33,12 +42,20 @@ const themes = { } } +const GlobalStyle = createGlobalStyle` + .highlight { + color:#007EE6; + } +`; + const Main = styled.div` - all:initial; - font-family: sans-serif; + all: initial; + * { + font-family: 'Geist', sans-serif; + } ` -const TextField = styled.input<{ inputWidth: string }>` - padding: 6px 6px; +const SearchButton = styled.button<{ inputWidth: string }>` + padding: 6px 6px; width: ${({ inputWidth }) => inputWidth}; border-radius: 8px; display: inline; @@ -50,14 +67,15 @@ const TextField = styled.input<{ inputWidth: string }>` -moz-appearance: none; appearance: none; transition: background-color 128ms linear; + text-align: left; &:focus { - outline: none; - box-shadow: - 0px 0px 0px 2px rgba(0, 109, 199), - 0px 0px 6px rgb(0, 90, 163), - 0px 2px 6px rgba(0, 0, 0, 0.1) ; - background-color: ${props => props.theme.primary.bg}; - } + outline: none; + box-shadow: + 0px 0px 0px 2px rgba(0, 109, 199), + 0px 0px 6px rgb(0, 90, 163), + 0px 2px 6px rgba(0, 0, 0, 0.1); + background-color: ${props => props.theme.primary.bg}; + } ` const Container = styled.div` @@ -65,51 +83,122 @@ const Container = styled.div` display: inline-block; ` const SearchResults = styled.div` - position: absolute; - display: block; + position: fixed; + display: flex; + flex-direction: column; background-color: ${props => props.theme.primary.bg}; - border: 1px solid rgba(0, 0, 0, .1); + border: 1px solid ${props => props.theme.secondary.text}; border-radius: 12px; padding: 8px; - width: 576px; - min-width: 96%; + width: 792px; + max-width: 90vw; + height: 70vh; z-index: 100; - height: 25vh; - overflow-y: auto; - top: 32px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); color: ${props => props.theme.primary.text}; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(16px); + box-sizing: border-box; + + @media only screen and (max-width: 768px) { + height: 80vh; + width: 90vw; + } +`; + +const SearchResultsScroll = styled.div` + flex: 1; + overflow-y: auto; + overflow-x: hidden; scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); scrollbar-gutter: stable; scrollbar-width: thin; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); - backdrop-filter: blur(16px); - @media only screen and (max-width: 768px) { - max-height: 100vh; - max-width: 80vw; - overflow: auto; + padding: 0 16px; +`; + +const ResultHeader = styled.div` + display: flex; + align-items: center; +`; + +const IconContainer = styled.div` + display: flex; + gap: 20px; + align-items: center; + margin-right: 20px; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 24px; + bottom: 0; + left: 50%; + width: 1px; + background-color: ${props => props.theme.secondary.text}; } -` +`; + +const IconTitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + const Title = styled.h3` - font-size: 14px; + font-size: 17.32px; + font-weight: 400; color: ${props => props.theme.primary.text}; - opacity: 0.8; - padding-bottom: 6px; - font-weight: 600; - text-transform: uppercase; - border-bottom: 1px solid ${(props) => props.theme.secondary.text}; -` + margin: 0; +`; +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; // Reduced from 1 +`; const Content = styled.div` - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + display: flex; + margin-left: 10px; + flex-direction: column; + gap: 8px; + padding: 4px 0 0px 20px; + font-size: 17.32px; + color: ${props => props.theme.primary.text}; + line-height: 1.6; + border-left: 2px solid #585858; ` +const ContentSegment = styled.div` + display: flex; + align-items: flex-start; + gap: 8px; + padding-right: 16px; +` +const TextContent = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; + padding-top: 3px; +`; + const ResultWrapper = styled.div` - padding: 4px 8px 4px 8px; - border-radius: 8px; + display: flex; + align-items: flex-start; + width: 100%; + box-sizing: border-box; + padding: 12px 16px 0 16px; cursor: pointer; - &.contains-source:hover{ + margin-bottom: 8px; + background-color: ${props => props.theme.primary.bg}; + transition: background-color 0.2s; + + &.contains-source:hover { background-color: rgba(0, 92, 197, 0.15); ${Title} { - color: rgb(0, 126, 230); - } + color: rgb(0, 126, 230); + } } ` const Markdown = styled.div` @@ -200,19 +289,71 @@ const NoResults = styled.div` font-size: 1rem; color: #888; `; -const InfoButton = styled.button` - cursor: pointer; - padding: 10px 4px 10px 4px; - display: block; +const AskAIButton = styled.button` + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; width: 100%; - color: inherit; + box-sizing: border-box; + height: 50px; + padding: 8px 24px; + border: none; border-radius: 6px; - background-color: ${(props) => props.theme.bg}; - text-align: center; + background-color: ${props => props.theme.secondary.bg}; + color: ${props => props.theme.bg === '#000' ? '#EDEDED' : props.theme.secondary.text}; + cursor: pointer; + transition: background-color 0.2s, box-shadow 0.2s; + font-size: 18px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 16px; + + &:hover { + opacity: 0.8; + } +` +const SearchHeader = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid ${props => props.theme.secondary.text}; +` + +const TextField = styled.input` + width: calc(100% - 32px); + margin: 0 16px; + padding: 12px 16px; + border: none; + background-color: transparent; + color: #EDEDED; + font-size: 22px; + font-weight: 400; + outline: none; + + &:focus { + border-color: none; + } +` + +const EscapeInstruction = styled.kbd` + display: flex; + align-items: center; + justify-content: center; + margin: 12px 16px 0; + padding: 4px 8px; + border-radius: 4px; + background-color: transparent; + border: 1px solid ${props => props.theme.secondary.text}; + color: ${props => props.theme.secondary.text}; font-size: 14px; - margin-bottom: 8px; - border:1px solid ${(props) => props.theme.secondary.text}; - + white-space: nowrap; + cursor: pointer; + width: fit-content; + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } ` export const SearchBar = ({ apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", @@ -226,47 +367,48 @@ export const SearchBar = ({ const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null); const containerRef = React.useRef(null); - const [isResultVisible, setIsResultVisible] = React.useState(true); + const [isResultVisible, setIsResultVisible] = React.useState(false); const [results, setResults] = React.useState([]); const debounceTimeout = React.useRef | null>(null); - const abortControllerRef = React.useRef(null) + const abortControllerRef = React.useRef(null); const browserOS = getOS(); - function isTouchDevice() { - return 'ontouchstart' in window; - } - const isTouch = isTouchDevice(); + const isTouch = 'ontouchstart' in window; + const md = new MarkdownIt(); + const getKeyboardInstruction = () => { - if (isResultVisible) return "Enter" - if (browserOS === 'mac') - return "⌘ K" - else - return "Ctrl K" - } + if (isResultVisible) return "Enter"; + return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K'; + }; + React.useEffect(() => { - const handleFocusSearch = (event: KeyboardEvent) => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsResultVisible(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { if ( ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || (browserOS === 'mac' && event.metaKey && event.key === 'k') ) { event.preventDefault(); inputRef.current?.focus(); - } - } - const handleClickOutside = (event: MouseEvent) => { - if ( - containerRef.current && - !containerRef.current.contains(event.target as Node) - ) { + setIsResultVisible(true); + } else if (event.key === 'Escape') { setIsResultVisible(false); } }; + + document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleFocusSearch); + document.addEventListener('keydown', handleKeyDown); return () => { - setIsResultVisible(true); document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); }; - }, []) + }, []); + React.useEffect(() => { if (!input) { setResults([]); @@ -291,8 +433,6 @@ export const SearchBar = ({ }, 500); return () => { - console.log(results); - abortController.abort(); clearTimeout(debounceTimeout.current ?? undefined); }; @@ -304,73 +444,105 @@ export const SearchBar = ({ openWidget(); } }; + const openWidget = () => { setIsWidgetOpen(true); - setIsResultVisible(false) - } + setIsResultVisible(false); + }; + const handleClose = () => { setIsWidgetOpen(false); - } - const md = new MarkdownIt(); + }; + return (
+ - setIsResultVisible(true)} inputWidth={width} - onFocus={() => setIsResultVisible(true)} - ref={inputRef} - onSubmit={() => setIsWidgetOpen(true)} - onKeyDown={(e) => handleKeyDown(e)} - placeholder={placeholder} - value={input} - onChange={(e) => setInput(e.target.value)} - /> + > + Search here + { - input.length > 0 && isResultVisible && ( + isResultVisible && ( - - { - isTouch ? - "Ask the AI" : - <> - Press Enter to ask the AI - - } - - {!loading ? - (results.length > 0 ? - results.map((res, key) => { - const containsSource = res.source !== 'local'; - const filteredResults = preprocessSearchResultsToHTML(res.text,input) - if (filteredResults) - return ( - { - if (!containsSource) return; - window.open(res.source, '_blank', 'noopener, noreferrer') - }} - className={containsSource ? "contains-source" : ""}> - {res.title} - - - - - ) - else { - setResults((prevItems) => prevItems.filter((_, index) => index !== key)); - } - }) - : - No results - ) - : - - } + + setInput(e.target.value)} + onKeyDown={(e) => handleKeyDown(e)} + placeholder={placeholder} + autoFocus + /> + setIsResultVisible(false)}> + Esc + + + + DocsGPT + Ask the AI + + + {!loading ? ( + results.length > 0 ? ( + results.map((res, key) => { + const containsSource = res.source !== 'local'; + const processedResults = processMarkdownString(res.text, input); + if (processedResults) + return ( + { + if (!containsSource) return; + window.open(res.source, '_blank', 'noopener, noreferrer'); + }} + > +
+ + + + {res.title} + + + {processedResults.map((element, index) => ( + + + {element.tag === 'code' && } + {(element.tag === 'bulletList' || element.tag === 'numberedList') && } + {element.tag === 'text' && } + {element.tag === 'heading' && } + {element.tag === 'blockquote' && } + +
+ + ))} + + +
+ + ); + return null; + }) + ) : ( + No results found + ) + ) : ( + + )} + ) } @@ -402,4 +574,4 @@ export const SearchBar = ({
) -} \ No newline at end of file +} diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts index ac257c91..9f92fdcb 100644 --- a/extensions/react-widget/src/utils/helper.ts +++ b/extensions/react-widget/src/utils/helper.ts @@ -25,7 +25,6 @@ export const getOS = () => { return 'other'; }; - interface ParsedElement { content: string; tag: string; @@ -48,14 +47,11 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse const trimmedLine = lines[i].trim(); if (!trimmedLine) continue; - // Handle code block start/end if (trimmedLine.startsWith('```')) { if (!isInCodeBlock) { - // Start of code block isInCodeBlock = true; codeBlockContent = []; } else { - // End of code block - process the collected content isInCodeBlock = false; const codeContent = codeBlockContent.join('\n'); const parsedElement: ParsedElement = { @@ -75,7 +71,6 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse continue; } - // Collect code block content if (isInCodeBlock) { codeBlockContent.push(trimmedLine); continue; @@ -86,7 +81,7 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/); const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/); const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); - const blockquoteMatch = trimmedLine.match(/^>+\s*(.+)$/); // Updated regex to handle multiple '>' symbols + const blockquoteMatch = trimmedLine.match(/^>+\s*(.+)$/); let content = trimmedLine; @@ -154,27 +149,3 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse return firstLine ? [firstLine] : []; }; - - -const markdownString = ` -# Title -This is a paragraph. - -## Subtitle -- Bullet item 1 -* Bullet item 2 -1. Numbered item 1 -2. Numbered item 2 - -\`\`\`javascript -const hello = "world"; -console.log(hello); -// This is a multi-line -// code block -\`\`\` - -Regular text after code block -`; - -const parsed = processMarkdownString(markdownString, 'world'); -console.log(parsed);