From b8fade251bb5af4750c70ff7dc79607ca501fe7f Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 24 Dec 2024 17:15:17 +0530 Subject: [PATCH 1/9] (feat: searchResults): adding utility to preprocess markdown --- extensions/react-widget/src/utils/helper.ts | 191 ++++++++++++++++++-- 1 file changed, 180 insertions(+), 11 deletions(-) diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts index d9aa19c34..511b39fce 100644 --- a/extensions/react-widget/src/utils/helper.ts +++ b/extensions/react-widget/src/utils/helper.ts @@ -27,22 +27,169 @@ export const getOS = () => { return 'other'; }; -export const preprocessSearchResultsToHTML = (text: string, keyword: string) => { - const md = new MarkdownIt(); - const htmlString = md.render(text); +interface MarkdownElement { + type: 'heading' | 'paragraph' | 'code' | 'list' | 'other'; + content: string; + level?: number; +} + +interface ParsedElement { + content: string; + tag: string; +} + +export const processMarkdownString = (markdown: string): ParsedElement[] => { + const result: ParsedElement[] = []; + const lines = markdown.trim().split('\n'); + + let isInCodeBlock = false; + let currentCodeBlock = ''; + + for (let i = 0; i < lines.length; i++) { + const trimmedLine = lines[i].trim(); + if (!trimmedLine) continue; + + if (trimmedLine.startsWith('```')) { + if (isInCodeBlock) { + if (currentCodeBlock.trim()) { + result.push({ + content: currentCodeBlock.trim(), + tag: 'code' + }); + } + currentCodeBlock = ''; + isInCodeBlock = false; + } else { + isInCodeBlock = true; + } + continue; + } + + if (isInCodeBlock) { + currentCodeBlock += trimmedLine + '\n'; + continue; + } + + const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + result.push({ + content: headingMatch[2], + tag: 'heading' + }); + continue; + } + + const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/); + if (bulletMatch) { + result.push({ + content: bulletMatch[1], + tag: 'bulletList' + }); + continue; + } - // Container for processed HTML - const filteredResults = document.createElement("div"); - filteredResults.innerHTML = htmlString; + const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); + if (numberedMatch) { + result.push({ + content: numberedMatch[1], + tag: 'numberedList' + }); + continue; + } + + result.push({ + content: trimmedLine, + tag: 'text' + }); + } - if (!processNode(filteredResults, keyword.trim())) return null; + if (isInCodeBlock && currentCodeBlock.trim()) { + result.push({ + content: currentCodeBlock.trim(), + tag: 'code' + }); + } - return filteredResults.innerHTML.trim() ? filteredResults.outerHTML : null; + return result; }; +export const preprocessSearchResultsToHTML = (text: string, keyword: string): MarkdownElement[] | null => { + const md = new MarkdownIt(); + const tokens = md.parse(text, {}); + const results: MarkdownElement[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token.type.endsWith('_close') || !token.content) continue; + + const content = token.content.toLowerCase(); + const keywordLower = keyword.trim().toLowerCase(); + + if (!content.includes(keywordLower)) continue; + + switch (token.type) { + case 'heading_open': + const level = parseInt(token.tag.charAt(1)); + const headingContent = tokens[i + 1].content; + results.push({ + type: 'heading', + content: headingContent, + level + }); + break; + + case 'paragraph_open': + const paragraphContent = tokens[i + 1].content; + results.push({ + type: 'paragraph', + content: paragraphContent + }); + break; + + case 'fence': + case 'code_block': + results.push({ + type: 'code', + content: token.content + }); + break; + + case 'bullet_list_open': + case 'ordered_list_open': + let listItems = []; + i++; + while (i < tokens.length && !tokens[i].type.includes('list_close')) { + if (tokens[i].type === 'list_item_open') { + i++; + if (tokens[i].content) { + listItems.push(tokens[i].content); + } + } + i++; + } + if (listItems.length > 0) { + results.push({ + type: 'list', + content: listItems.join('\n') + }); + } + break; + + default: + if (token.content) { + results.push({ + type: 'other', + content: token.content + }); + } + break; + } + } + return results.length > 0 ? results : null; +}; -// Recursive function to process nodes const processNode = (node: Node, keyword: string): boolean => { const keywordRegex = new RegExp(`(${keyword})`, "gi"); @@ -57,7 +204,6 @@ const processNode = (node: Node, keyword: string): boolean => { const tempContainer = document.createElement("div"); tempContainer.innerHTML = highlightedHTML; - // Replace the text node with highlighted content while (tempContainer.firstChild) { node.parentNode?.insertBefore(tempContainer.firstChild, node); } @@ -84,4 +230,27 @@ const processNode = (node: Node, keyword: string): boolean => { } return false; -}; \ No newline at end of file +}; + +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); +console.log(parsed); From 2420af3b6d3c4c98a75b3f624e204c9514cac7d5 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 27 Dec 2024 16:19:33 +0530 Subject: [PATCH 2/9] (feat:Search) highlight keywords on searching --- extensions/react-widget/src/utils/helper.ts | 247 +++++++------------- 1 file changed, 82 insertions(+), 165 deletions(-) diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts index 511b39fce..13d2bf7a7 100644 --- a/extensions/react-widget/src/utils/helper.ts +++ b/extensions/react-widget/src/utils/helper.ts @@ -1,5 +1,3 @@ -import MarkdownIt from "markdown-it"; -import DOMPurify from "dompurify"; export const getOS = () => { const platform = window.navigator.platform; const userAgent = window.navigator.userAgent || window.navigator.vendor; @@ -27,211 +25,130 @@ export const getOS = () => { return 'other'; }; -interface MarkdownElement { - type: 'heading' | 'paragraph' | 'code' | 'list' | 'other'; - content: string; - level?: number; -} interface ParsedElement { content: string; tag: string; } -export const processMarkdownString = (markdown: string): ParsedElement[] => { - const result: ParsedElement[] = []; +export const processMarkdownString = (markdown: string, keyword?: string): ParsedElement[] => { const lines = markdown.trim().split('\n'); - + const keywordLower = keyword?.toLowerCase(); + + const escapeRegExp = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + const escapedKeyword = keyword ? escapeRegExp(keyword) : ''; + const keywordRegex = keyword ? new RegExp(`(${escapedKeyword})`, 'gi') : null; + let isInCodeBlock = false; - let currentCodeBlock = ''; + let codeBlockContent: string[] = []; + let matchingLines: ParsedElement[] = []; + let firstLine: ParsedElement | null = null; for (let i = 0; i < lines.length; i++) { const trimmedLine = lines[i].trim(); if (!trimmedLine) continue; + // Handle code block start/end if (trimmedLine.startsWith('```')) { - if (isInCodeBlock) { - if (currentCodeBlock.trim()) { - result.push({ - content: currentCodeBlock.trim(), - tag: 'code' - }); - } - currentCodeBlock = ''; - isInCodeBlock = false; - } else { + 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 = { + content: codeContent, + tag: 'code' + }; + + if (!firstLine) { + firstLine = parsedElement; + } + + if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); + } } continue; } + // Collect code block content if (isInCodeBlock) { - currentCodeBlock += trimmedLine + '\n'; + codeBlockContent.push(trimmedLine); continue; } + let parsedElement: ParsedElement | null = null; + const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/); + const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/); + const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); + + let content = trimmedLine; + if (headingMatch) { - result.push({ - content: headingMatch[2], + content = headingMatch[2]; + parsedElement = { + content: content, tag: 'heading' - }); - continue; - } - - const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/); - if (bulletMatch) { - result.push({ - content: bulletMatch[1], + }; + } else if (bulletMatch) { + content = bulletMatch[1]; + parsedElement = { + content: content, tag: 'bulletList' - }); - continue; + }; + } else if (numberedMatch) { + content = numberedMatch[1]; + parsedElement = { + content: content, + tag: 'numberedList' + }; + } else { + parsedElement = { + content: content, + tag: 'text' + }; } - const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); - if (numberedMatch) { - result.push({ - content: numberedMatch[1], - tag: 'numberedList' - }); - continue; + if (!firstLine) { + firstLine = parsedElement; } - result.push({ - content: trimmedLine, - tag: 'text' - }); + if (keywordLower && parsedElement.content.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); + } } - if (isInCodeBlock && currentCodeBlock.trim()) { - result.push({ - content: currentCodeBlock.trim(), + if (isInCodeBlock && codeBlockContent.length > 0) { + const codeContent = codeBlockContent.join('\n'); + const parsedElement: ParsedElement = { + content: codeContent, tag: 'code' - }); - } - - return result; -}; + }; -export const preprocessSearchResultsToHTML = (text: string, keyword: string): MarkdownElement[] | null => { - const md = new MarkdownIt(); - const tokens = md.parse(text, {}); - const results: MarkdownElement[] = []; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (token.type.endsWith('_close') || !token.content) continue; - - const content = token.content.toLowerCase(); - const keywordLower = keyword.trim().toLowerCase(); - - if (!content.includes(keywordLower)) continue; - - switch (token.type) { - case 'heading_open': - const level = parseInt(token.tag.charAt(1)); - const headingContent = tokens[i + 1].content; - results.push({ - type: 'heading', - content: headingContent, - level - }); - break; - - case 'paragraph_open': - const paragraphContent = tokens[i + 1].content; - results.push({ - type: 'paragraph', - content: paragraphContent - }); - break; - - case 'fence': - case 'code_block': - results.push({ - type: 'code', - content: token.content - }); - break; - - case 'bullet_list_open': - case 'ordered_list_open': - let listItems = []; - i++; - while (i < tokens.length && !tokens[i].type.includes('list_close')) { - if (tokens[i].type === 'list_item_open') { - i++; - if (tokens[i].content) { - listItems.push(tokens[i].content); - } - } - i++; - } - if (listItems.length > 0) { - results.push({ - type: 'list', - content: listItems.join('\n') - }); - } - break; - - default: - if (token.content) { - results.push({ - type: 'other', - content: token.content - }); - } - break; + if (!firstLine) { + firstLine = parsedElement; } - } - - return results.length > 0 ? results : null; -}; -const processNode = (node: Node, keyword: string): boolean => { - - const keywordRegex = new RegExp(`(${keyword})`, "gi"); - if (node.nodeType === Node.TEXT_NODE) { - const textContent = node.textContent || ""; - - if (textContent.toLowerCase().includes(keyword.toLowerCase())) { - const highlightedHTML = textContent.replace( - keywordRegex, - `$1` - ); - const tempContainer = document.createElement("div"); - tempContainer.innerHTML = highlightedHTML; - - while (tempContainer.firstChild) { - node.parentNode?.insertBefore(tempContainer.firstChild, node); - } - node.parentNode?.removeChild(node); - - return true; + if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { + parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + matchingLines.push(parsedElement); } + } - return false; - } else if (node.nodeType === Node.ELEMENT_NODE) { - - const children = Array.from(node.childNodes); - let hasKeyword = false; - - children.forEach((child) => { - if (!processNode(child, keyword)) { - node.removeChild(child); - } else { - hasKeyword = true; - } - }); - - return hasKeyword; + if (keywordLower && matchingLines.length > 0) { + return matchingLines; } - return false; + return firstLine ? [firstLine] : []; }; + const markdownString = ` # Title This is a paragraph. @@ -252,5 +169,5 @@ console.log(hello); Regular text after code block `; -const parsed = processMarkdownString(markdownString); +const parsed = processMarkdownString(markdownString, 'world'); console.log(parsed); From 5ddf9bd7ecb36e3ca1cf39c3ecff69bd3fad482e Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 27 Dec 2024 17:40:04 +0530 Subject: [PATCH 3/9] (feat:search) handle blockquotes in markdown --- extensions/react-widget/src/utils/helper.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts index 13d2bf7a7..ac257c91a 100644 --- a/extensions/react-widget/src/utils/helper.ts +++ b/extensions/react-widget/src/utils/helper.ts @@ -86,6 +86,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 let content = trimmedLine; @@ -107,6 +108,12 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse content: content, tag: 'numberedList' }; + } else if (blockquoteMatch) { + content = blockquoteMatch[1]; + parsedElement = { + content: content, + tag: 'blockquote' + }; } else { parsedElement = { content: content, From 8724c12c11e081aba4976f683c81e7d4e071b72b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 31 Dec 2024 15:30:24 +0530 Subject: [PATCH 4/9] (feat:search) new UI --- .../react-widget/src/components/SearchBar.tsx | 442 ++++++++++++------ extensions/react-widget/src/utils/helper.ts | 31 +- 2 files changed, 308 insertions(+), 165 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 42262e08c..5982f6f60 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 ac257c91a..9f92fdcbb 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); From 598c7a5d76829b71dc24fdaa6500407a81b1d5eb Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 31 Dec 2024 16:25:44 +0530 Subject: [PATCH 5/9] (feat:search) load geist font --- .../react-widget/src/components/SearchBar.tsx | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 5982f6f60..b887c17d9 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { WidgetCore } from './DocsGPTWidget'; import { SearchBarProps } from '@/types'; @@ -48,11 +48,16 @@ const GlobalStyle = createGlobalStyle` } `; +const loadGeistFont = () => { + const link = document.createElement('link'); + link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; // Replace with the actual CDN URL + link.rel = 'stylesheet'; + document.head.appendChild(link); +}; + const Main = styled.div` all: initial; - * { - font-family: 'Geist', sans-serif; - } + font-family: 'Geist', sans-serif; ` const SearchButton = styled.button<{ inputWidth: string }>` padding: 6px 6px; @@ -118,29 +123,6 @@ const SearchResultsScroll = styled.div` 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; @@ -156,7 +138,7 @@ const Title = styled.h3` const ContentWrapper = styled.div` display: flex; flex-direction: column; - gap: 8px; // Reduced from 1 + gap: 8px; `; const Content = styled.div` display: flex; @@ -192,6 +174,7 @@ const ResultWrapper = styled.div` cursor: pointer; margin-bottom: 8px; background-color: ${props => props.theme.primary.bg}; + font-family: 'Geist',sans-serif; transition: background-color 0.2s; &.contains-source:hover { @@ -381,6 +364,7 @@ export const SearchBar = ({ }; React.useEffect(() => { + loadGeistFont() const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsResultVisible(false); From 2f33a46e89f76aa3e18807264ba87c006c1f4f53 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 31 Dec 2024 17:33:06 +0530 Subject: [PATCH 6/9] (feat:search/UX) enhance function --- .../react-widget/src/components/SearchBar.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index b887c17d9..18ac88a87 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -50,7 +50,7 @@ const GlobalStyle = createGlobalStyle` const loadGeistFont = () => { const link = document.createElement('link'); - link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; // Replace with the actual CDN URL + link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; link.rel = 'stylesheet'; document.head.appendChild(link); }; @@ -61,6 +61,7 @@ const Main = styled.div` ` const SearchButton = styled.button<{ inputWidth: string }>` padding: 6px 6px; + font-family: inherit; width: ${({ inputWidth }) => inputWidth}; border-radius: 8px; display: inline; @@ -93,11 +94,11 @@ const SearchResults = styled.div` flex-direction: column; background-color: ${props => props.theme.primary.bg}; border: 1px solid ${props => props.theme.secondary.text}; - border-radius: 12px; - padding: 8px; + border-radius: 15px; + padding: 8px 0px 8px 0px; width: 792px; max-width: 90vw; - height: 70vh; + height: 415px; z-index: 100; left: 50%; top: 50%; @@ -277,7 +278,8 @@ const AskAIButton = styled.button` align-items: center; justify-content: flex-start; gap: 12px; - width: 100%; + width: calc(100% - 32px); + margin: 0 16px 16px 16px; box-sizing: border-box; height: 50px; padding: 8px 24px; @@ -289,7 +291,6 @@ const AskAIButton = styled.button` 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; @@ -331,6 +332,7 @@ const EscapeInstruction = styled.kbd` border: 1px solid ${props => props.theme.secondary.text}; color: ${props => props.theme.secondary.text}; font-size: 14px; + font-family: 'Geist', sans-serif; white-space: nowrap; cursor: pointer; width: fit-content; @@ -356,8 +358,7 @@ export const SearchBar = ({ const abortControllerRef = React.useRef(null); const browserOS = getOS(); const isTouch = 'ontouchstart' in window; - const md = new MarkdownIt(); - + const getKeyboardInstruction = () => { if (isResultVisible) return "Enter"; return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K'; @@ -436,6 +437,7 @@ export const SearchBar = ({ const handleClose = () => { setIsWidgetOpen(false); + setIsResultVisible(true); }; return ( From 190f57171875bcbbc0f31ff8e9ceec5d5a733a37 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Wed, 1 Jan 2025 15:14:03 +0530 Subject: [PATCH 7/9] (feat/search) exacting ui --- .../react-widget/src/components/SearchBar.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 18ac88a87..3a7f2ddd0 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -93,7 +93,7 @@ const SearchResults = styled.div` display: flex; flex-direction: column; background-color: ${props => props.theme.primary.bg}; - border: 1px solid ${props => props.theme.secondary.text}; + border: 1px solid ${props => props.theme.secondary.bg}; border-radius: 15px; padding: 8px 0px 8px 0px; width: 792px; @@ -128,6 +128,10 @@ const IconTitleWrapper = styled.div` display: flex; align-items: center; gap: 8px; + + .element-icon{ + margin: 4px; + } `; const Title = styled.h3` @@ -143,10 +147,10 @@ const ContentWrapper = styled.div` `; const Content = styled.div` display: flex; - margin-left: 10px; + margin-left: 8px; flex-direction: column; gap: 8px; - padding: 4px 0 0px 20px; + padding: 4px 0px 0px 12px; font-size: 17.32px; color: ${props => props.theme.primary.text}; line-height: 1.6; @@ -158,13 +162,6 @@ const ContentSegment = styled.div` 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` display: flex; @@ -173,7 +170,6 @@ const ResultWrapper = styled.div` box-sizing: border-box; padding: 12px 16px 0 16px; cursor: pointer; - margin-bottom: 8px; background-color: ${props => props.theme.primary.bg}; font-family: 'Geist',sans-serif; transition: background-color 0.2s; @@ -302,7 +298,7 @@ const SearchHeader = styled.div` gap: 8px; margin-bottom: 12px; padding-bottom: 12px; - border-bottom: 1px solid ${props => props.theme.secondary.text}; + border-bottom: 1px solid ${props => props.theme.secondary.bg}; ` const TextField = styled.input` From 8b206b087c782aac7c944e9dedfb7ee74a0e554b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 2 Jan 2025 19:36:07 +0530 Subject: [PATCH 8/9] (feat:search) adding buttonTextt prop, minor ui --- .../react-widget/src/components/SearchBar.tsx | 55 ++++++++----------- extensions/react-widget/src/types/index.ts | 7 ++- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 3a7f2ddd0..a9e439727 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -18,7 +18,7 @@ import { const themes = { dark: { bg: '#000', - text: '#fff', + text: '#EDEDED', primary: { text: "#FAFAFA", bg: '#111111' @@ -30,7 +30,7 @@ const themes = { }, light: { bg: '#fff', - text: '#000', + text: '#171717', primary: { text: "#222327", bg: "#fff" @@ -65,7 +65,7 @@ const SearchButton = styled.button<{ inputWidth: string }>` width: ${({ inputWidth }) => inputWidth}; border-radius: 8px; display: inline; - color: ${props => props.theme.primary.text}; + color: ${props => props.theme.secondary.text}; outline: none; border: none; background-color: ${props => props.theme.secondary.bg}; @@ -74,14 +74,6 @@ const SearchButton = styled.button<{ inputWidth: string }>` 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}; - } ` const Container = styled.div` @@ -98,7 +90,7 @@ const SearchResults = styled.div` padding: 8px 0px 8px 0px; width: 792px; max-width: 90vw; - height: 415px; + height: 396px; z-index: 100; left: 50%; top: 50%; @@ -118,9 +110,9 @@ 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; + scrollbar-color: #383838 transparent; padding: 0 16px; `; @@ -135,7 +127,7 @@ const IconTitleWrapper = styled.div` `; const Title = styled.h3` - font-size: 17.32px; + font-size: 15px; font-weight: 400; color: ${props => props.theme.primary.text}; margin: 0; @@ -151,7 +143,7 @@ const Content = styled.div` flex-direction: column; gap: 8px; padding: 4px 0px 0px 12px; - font-size: 17.32px; + font-size: 15px; color: ${props => props.theme.primary.text}; line-height: 1.6; border-left: 2px solid #585858; @@ -182,13 +174,13 @@ const ResultWrapper = styled.div` } ` const Markdown = styled.div` -line-height:20px; -font-size: 12px; +line-height:18px; +font-size: 11px; white-space: pre-wrap; pre { padding: 8px; width: 90%; - font-size: 12px; + font-size: 11px; border-radius: 6px; overflow-x: auto; background-color: #1B1C1F; @@ -196,7 +188,7 @@ white-space: pre-wrap; } h1,h2 { - font-size: 16px; + font-size: 14px; font-weight: 600; color: ${(props) => props.theme.text}; opacity: 0.8; @@ -204,20 +196,20 @@ white-space: pre-wrap; h3 { - font-size: 14px; + font-size: 12px; } p { margin: 0px; line-height: 1.35rem; - font-size: 12px; + font-size: 11px; } code:not(pre code) { border-radius: 6px; padding: 2px 2px; margin: 2px; - font-size: 10px; + font-size: 9px; display: inline; background-color: #646464; color: #fff ; @@ -266,7 +258,7 @@ const Loader = styled.div` const NoResults = styled.div` margin-top: 2rem; text-align: center; - font-size: 1rem; + font-size: 14px; color: #888; `; const AskAIButton = styled.button` @@ -282,10 +274,10 @@ const AskAIButton = styled.button` border: none; border-radius: 6px; background-color: ${props => props.theme.secondary.bg}; - color: ${props => props.theme.bg === '#000' ? '#EDEDED' : props.theme.secondary.text}; + color: ${props => props.theme.text}; cursor: pointer; transition: background-color 0.2s, box-shadow 0.2s; - font-size: 18px; + font-size: 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); &:hover { @@ -307,8 +299,8 @@ const TextField = styled.input` padding: 12px 16px; border: none; background-color: transparent; - color: #EDEDED; - font-size: 22px; + color: ${props => props.theme.text}; + font-size: 20px; font-weight: 400; outline: none; @@ -326,8 +318,8 @@ const EscapeInstruction = styled.kbd` border-radius: 4px; background-color: transparent; border: 1px solid ${props => props.theme.secondary.text}; - color: ${props => props.theme.secondary.text}; - font-size: 14px; + color: ${props => props.theme.text}; + font-size: 12px; font-family: 'Geist', sans-serif; white-space: nowrap; cursor: pointer; @@ -341,7 +333,8 @@ export const SearchBar = ({ apiHost = "https://gptcloud.arc53.com", theme = "dark", placeholder = "Search or Ask AI...", - width = "256px" + width = "256px", + buttonText = "Search here" }: SearchBarProps) => { const [input, setInput] = React.useState(""); const [loading, setLoading] = React.useState(false); @@ -445,7 +438,7 @@ export const SearchBar = ({ onClick={() => setIsResultVisible(true)} inputWidth={width} > - Search here + {buttonText} { isResultVisible && ( diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index cea9e43a3..5438cec73 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -44,9 +44,10 @@ export interface WidgetCoreProps extends WidgetProps { export interface SearchBarProps { apiHost?: string; apiKey?: string; - theme?:THEME; - placeholder?:string; - width?:string; + theme?: THEME; + placeholder?: string; + width?: string; + buttonText?: string; } export interface Result { From 411115523ee0bceba3cc731a154aab92820a9612 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 3 Jan 2025 02:49:40 +0530 Subject: [PATCH 9/9] (fix:search) ui adjustments --- .../react-widget/src/components/SearchBar.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index a9e439727..7ab9c0cee 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -74,6 +74,7 @@ const SearchButton = styled.button<{ inputWidth: string }>` appearance: none; transition: background-color 128ms linear; text-align: left; + cursor: pointer; ` const Container = styled.div` @@ -131,6 +132,10 @@ const Title = styled.h3` font-weight: 400; color: ${props => props.theme.primary.text}; margin: 0; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; `; const ContentWrapper = styled.div` display: flex; @@ -147,12 +152,17 @@ const Content = styled.div` color: ${props => props.theme.primary.text}; line-height: 1.6; border-left: 2px solid #585858; + overflow: hidden; ` const ContentSegment = styled.div` display: flex; align-items: flex-start; gap: 8px; padding-right: 16px; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; ` const ResultWrapper = styled.div` @@ -166,6 +176,13 @@ const ResultWrapper = styled.div` font-family: 'Geist',sans-serif; transition: background-color 0.2s; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; + &.contains-source:hover { background-color: rgba(0, 92, 197, 0.15); ${Title} { @@ -303,7 +320,7 @@ const TextField = styled.input` font-size: 20px; font-weight: 400; outline: none; - + &:focus { border-color: none; }