From 541a6417b707f6a19527f7b0330a9e95c864323a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 14 Nov 2024 04:51:26 +0530 Subject: [PATCH 01/21] (refactor): separate widget core --- .../src/components/DocsGPTWidget.tsx | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 1baa4c62..355056ac 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useRef } from 'react' import DOMPurify from 'dompurify'; -import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; -import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetProps } from '../types/index'; +import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; import { ThemeProvider } from 'styled-components'; import Like from "../assets/like.svg" @@ -66,13 +66,14 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` right: ${props => props.modal ? '50%' : '10px'}; bottom: ${props => props.modal ? '50%' : '10px'}; z-index: 1000; - display: none; transform-origin:100% 100%; &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + display: block; } &.close { animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + display: none; } ${props => props.modal && "transform : translate(50%,50%);" @@ -119,11 +120,11 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); padding: 26px 26px 0px 26px; animation: ${({ isOpen, theme }) => - theme.dimensions.size === 'large' - ? isOpen - ? 'fadeIn 150ms ease-in forwards' - : 'fadeOut 150ms ease-in forwards' - : isOpen + theme.dimensions.size === 'large' + ? isOpen + ? 'fadeIn 150ms ease-in forwards' + : 'fadeOut 150ms ease-in forwards' + : isOpen ? 'openContainer 150ms ease-in forwards' : 'closeContainer 250ms ease-in forwards'}; @keyframes openContainer { @@ -478,7 +479,49 @@ const Hero = ({ title, description, theme }: { title: string, description: strin ); }; -export const DocsGPTWidget = ({ +export const DocsGPTWidget = (props: WidgetProps) => { + + const { + buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', + buttonText = 'Ask a question', + buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', + defaultOpen = false, + ...coreProps + } = props + + const [open, setOpen] = React.useState(defaultOpen); + const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); + const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); + const widgetRef = useRef(null) + + + const handleClose = () => { + setOpen(false); + setTimeout(() => { + if (widgetRef.current) + widgetRef.current.style.display = "none"; + setIsFloatingButtonVisible(true); + setIsAnimatingButton(true); + setTimeout(() => setIsAnimatingButton(false), 200); + }, 250) + }; + const handleOpen = () => { + setOpen(true); + setIsFloatingButtonVisible(false); + if (widgetRef.current) + widgetRef.current.style.display = 'block' + } + return ( + <> + + + + ) +} +export const WidgetCore = ({ apiHost = 'https://gptcloud.arc53.com', apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a', avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png', @@ -488,22 +531,19 @@ export const DocsGPTWidget = ({ heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.', size = 'small', theme = 'dark', - buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', - buttonText = 'Ask a question', - buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', collectFeedback = true, - deafultOpen = false -}: WidgetProps) => { - const [prompt, setPrompt] = React.useState(''); + widgetRef = null, + isOpen = false, + prefilledQuery = "", + handleClose +}: WidgetCoreProps) => { + const [prompt, setPrompt] = React.useState(prefilledQuery); const [status, setStatus] = React.useState('idle'); - const [queries, setQueries] = React.useState([]) - const [conversationId, setConversationId] = React.useState(null) - const [open, setOpen] = React.useState(deafultOpen) + const [queries, setQueries] = React.useState([]); + const [conversationId, setConversationId] = React.useState(null); const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling - const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); - const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); - const isBubbleHovered = useRef(false) - const widgetRef = useRef(null) + + const isBubbleHovered = useRef(false); const endMessageRef = React.useRef(null); const md = new MarkdownIt(); @@ -615,37 +655,17 @@ export const DocsGPTWidget = ({ const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; - const handleClose = () => { - setOpen(false); - setTimeout(() => { - if (widgetRef.current) widgetRef.current.style.display = "none"; - setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); - setTimeout(() => setIsAnimatingButton(false), 200); - }, 250) - }; - const handleOpen = () => { - setOpen(true); - setIsFloatingButtonVisible(false); - if (widgetRef.current) - widgetRef.current.style.display = 'block' - } const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; - return ( - {open && size === 'large' && + {isOpen && size === 'large' && } - - - { + + {
From 9409e4498f7f4579d25f5ab8d4a0e5a7ef903180 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 14 Nov 2024 04:52:15 +0530 Subject: [PATCH 02/21] (feat:search bar) initiating seach bar --- extensions/react-widget/package.json | 2 +- extensions/react-widget/src/App.tsx | 8 +- .../react-widget/src/components/SearchBar.tsx | 177 ++++++++++++++++++ extensions/react-widget/src/index.html | 6 +- extensions/react-widget/src/index.ts | 3 +- extensions/react-widget/src/main.tsx | 21 ++- .../react-widget/src/requests/searchAPI.ts | 34 ++++ extensions/react-widget/src/types/index.ts | 21 ++- 8 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 extensions/react-widget/src/components/SearchBar.tsx create mode 100644 extensions/react-widget/src/requests/searchAPI.ts diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index a1097403..554b2438 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -32,7 +32,7 @@ "scripts": { "build": "parcel build src/main.tsx --public-url ./", "build:react": "parcel build src/index.ts", - "dev": "parcel src/index.html -p 3000", + "dev": "parcel -p 3000", "test": "jest", "lint": "eslint", "check": "tsc --noEmit", diff --git a/extensions/react-widget/src/App.tsx b/extensions/react-widget/src/App.tsx index ec9de47b..4bb24bae 100644 --- a/extensions/react-widget/src/App.tsx +++ b/extensions/react-widget/src/App.tsx @@ -1,11 +1,11 @@ import React from "react" import {DocsGPTWidget} from "./components/DocsGPTWidget" -const App = () => { +import {SearchBar} from "./components/SearchBar" +export const App = () => { return (
+
) -} - -export default App \ No newline at end of file +} \ No newline at end of file diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx new file mode 100644 index 00000000..f16271a3 --- /dev/null +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -0,0 +1,177 @@ +import React from 'react' +import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import { WidgetCore } from './DocsGPTWidget'; +import { SearchBarProps } from '@/types'; +import { getSearchResults } from '../requests/searchAPI' +import { Result } from '@/types'; +import MarkdownIt from 'markdown-it'; +import DOMPurify from 'dompurify'; +const Main = styled.div` + font-family: sans-serif; +` +const TextField = styled.input` + padding: 8px; + border-radius: 8px; + display: inline; + color: rgb(107 114 128); + outline: none; + border: 2px solid transparent; + background-color: rgba(0, 0, 0, .05);; + &:focus { + outline: #467f95; /* remove default outline */ + border:2px solid skyblue; /* change border color on focus */ + box-shadow: 0px 0px 2px skyblue; /* add a red box shadow on focus */ + } +` + +const Container = styled.div` + position: relative; + display: inline-block; +` +const SearchResults = styled.div` + position: absolute; + background-color: white; + opacity: 90%; + border: 1px solid rgba(0, 0, 0, .1); + border-radius: 12px; + padding: 20px; + width: 576px; + z-index: 100; + height: 25vh; + overflow-y: auto; + top: 45px; + 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); +` +const Title = styled.h3` + font-size: 12px; + color: rgb(107, 114, 128); + padding-bottom: 4px; + font-weight: 600; + text-transform: uppercase; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + +` + +const Content = styled.div` + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +` +const Markdown = styled.div` + +font-size: 12px; + pre { + padding: 8px; + width: 90%; + font-size: 12px; + border-radius: 6px; + overflow-x: auto; + background-color: #1B1C1F; + color: #fff ; + } + + h1,h2 { + font-size: 16px; + font-weight: 600; + color: rgb(31, 41, 55); + } + + + h3 { + font-size: 14px; + } + + p { + margin: 0px; + line-height: 1.35rem; + } + + code:not(pre code) { + border-radius: 6px; + padding: 4px 4px; + font-size: 12px; + display: inline-block; + background-color: #646464; + color: #fff ; + } + + code { + white-space: pre-wrap ; + overflow-wrap: break-word; + word-break: break-all; + } + + ul{ + padding:0px; + list-style-position: inside; + } +` +export const SearchBar = ({ + apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", + apiHost = "http://127.0.0.1:7091", + theme = "dark" +}: SearchBarProps) => { + const [input, setInput] = React.useState("") + const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); + const inputRef = React.useRef(null) + const [results, setResults] = React.useState([]) + React.useEffect(() => { + input.length > 0 ? + getSearchResults(input, apiKey, apiHost) + .then((data) => setResults(data)) + .catch((err) => console.log(err)) + : + setResults([]) + }, [input]) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + setIsWidgetOpen(true); + } + }; + const handleClose = () => { + setIsWidgetOpen(false); + } + const md = new MarkdownIt(); + return ( +
+ + handleKeyDown(e)} + placeholder='Search here or Ask DocsGPT' + value={input} + onChange={(e) => setInput(e.target.value)} + /> + { + results.length > 0 && ( + + {results.map((res) => ( +
+ {res.title} + + + +
+ )) + } +
+ + ) + } +
+ + +
+ ) +} \ No newline at end of file diff --git a/extensions/react-widget/src/index.html b/extensions/react-widget/src/index.html index 0f0710d5..40eaad15 100644 --- a/extensions/react-widget/src/index.html +++ b/extensions/react-widget/src/index.html @@ -9,11 +9,11 @@
- - --> diff --git a/extensions/react-widget/src/index.ts b/extensions/react-widget/src/index.ts index 1efa89a6..e29b85a5 100644 --- a/extensions/react-widget/src/index.ts +++ b/extensions/react-widget/src/index.ts @@ -1 +1,2 @@ -export { DocsGPTWidget } from "./components/DocsGPTWidget"; \ No newline at end of file +export {SearchBar} from "./components/SearchBar" +export { DocsGPTWidget } from "./components/DocsGPTWidget"; diff --git a/extensions/react-widget/src/main.tsx b/extensions/react-widget/src/main.tsx index 4fb3bbb4..a8542e26 100644 --- a/extensions/react-widget/src/main.tsx +++ b/extensions/react-widget/src/main.tsx @@ -1,12 +1,25 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { SearchBar } from './components/SearchBar'; +import React from "react"; if (typeof window !== 'undefined') { const renderWidget = (elementId: string, props = {}) => { const root = createRoot(document.getElementById(elementId) as HTMLElement); root.render(); }; + const renderSearchBar = (elementId: string, props = {}) => { + const root = createRoot(document.getElementById(elementId) as HTMLElement); + root.render(); + }; (window as any).renderDocsGPTWidget = renderWidget; + + (window as any).renderSearchBar = renderSearchBar; } -export { DocsGPTWidget }; \ No newline at end of file +const container = document.getElementById("app") as HTMLElement; +const root = createRoot(container) +root.render(); + +export { DocsGPTWidget }; +export { SearchBar } diff --git a/extensions/react-widget/src/requests/searchAPI.ts b/extensions/react-widget/src/requests/searchAPI.ts new file mode 100644 index 00000000..18df3691 --- /dev/null +++ b/extensions/react-widget/src/requests/searchAPI.ts @@ -0,0 +1,34 @@ +import { Result } from "@/types"; + + async function getSearchResults(question: string, apiKey:string, apiHost:string): Promise { + + const payload = { + question, + api_key:apiKey + }; + + try { + const response = await fetch(`${apiHost}/api/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); + } + + const data: Result[] = await response.json(); + return data; + + } catch (error) { + console.error("Failed to fetch documents:", error); + throw error; + } + } + + export { + getSearchResults + } \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 717efd92..445260dc 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -32,5 +32,22 @@ export interface WidgetProps { buttonText?:string; buttonBg?:string; collectFeedback?:boolean; - deafultOpen?: boolean; -} \ No newline at end of file + defaultOpen?: boolean; +} +export interface WidgetCoreProps extends WidgetProps { + widgetRef?:React.RefObject | null; + handleClose?:React.MouseEventHandler | undefined; + isOpen:boolean; + prefilledQuery?: string; +} + +export interface SearchBarProps { + apiHost?: string; + apiKey?: string; + theme?:THEME +} + +export interface Result { + text:string; + title:string +} From 811a20f080151fd781ac6710b005227d4397bc78 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 05:52:46 +0530 Subject: [PATCH 03/21] search: add themes, fix css override --- .../src/components/DocsGPTWidget.tsx | 6 +- .../react-widget/src/components/SearchBar.tsx | 150 +++++++++++------- extensions/react-widget/src/types/index.ts | 3 +- 3 files changed, 99 insertions(+), 60 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 355056ac..6fff4f46 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -106,8 +106,10 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` `; const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; - max-height: ${(props) => props.theme.dimensions.maxHeight}; - max-width: ${(props) => props.theme.dimensions.maxWidth}; + max-height: ${(props) => props.theme.dimensions.maxHeight} !important; + max-width: ${(props) => props.theme.dimensions.maxWidth} !important; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; position: relative; flex-direction: column; justify-content: space-between; diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index f16271a3..a6a48173 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -1,26 +1,56 @@ import React from 'react' -import styled, { keyframes, createGlobalStyle } from 'styled-components'; +import styled, { keyframes, createGlobalStyle, ThemeProvider } from 'styled-components'; import { WidgetCore } from './DocsGPTWidget'; import { SearchBarProps } from '@/types'; import { getSearchResults } from '../requests/searchAPI' import { Result } from '@/types'; import MarkdownIt from 'markdown-it'; import DOMPurify from 'dompurify'; + +const themes = { + dark: { + bg: '#222327', + text: '#fff', + primary: { + text: "#FAFAFA", + bg: '#111111' + }, + secondary: { + text: "#A1A1AA", + bg: "#38383b" + } + }, + light: { + bg: '#fff', + text: '#000', + primary: { + text: "#222327", + bg: "#fff" + }, + secondary: { + text: "#A1A1AA", + bg: "#F6F6F6" + } + } + } + const Main = styled.div` + all:initial; + font-family: sans-serif; ` const TextField = styled.input` - padding: 8px; + padding: 6px 6px; border-radius: 8px; display: inline; - color: rgb(107 114 128); + color: ${props => props.theme.primary.text}; outline: none; border: 2px solid transparent; - background-color: rgba(0, 0, 0, .05);; + background-color: ${props => props.theme.secondary.bg}; &:focus { - outline: #467f95; /* remove default outline */ - border:2px solid skyblue; /* change border color on focus */ - box-shadow: 0px 0px 2px skyblue; /* add a red box shadow on focus */ + border:2px solid #007ee6; + box-shadow: 0px 0px 2px skyblue; + background-color: ${props => props.theme.primary.bg}; } ` @@ -30,29 +60,30 @@ const Container = styled.div` ` const SearchResults = styled.div` position: absolute; - background-color: white; + background-color: ${props => props.theme.primary.bg}; opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); border-radius: 12px; - padding: 20px; + padding: 15px; width: 576px; z-index: 100; height: 25vh; overflow-y: auto; top: 45px; - scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); - scrollbar-gutter: stable; - scrollbar-width: thin; + color: ${props => props.theme.primary.text}; + 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); ` const Title = styled.h3` - font-size: 12px; + font-size: 14px; color: rgb(107, 114, 128); - padding-bottom: 4px; + padding-bottom: 6px; font-weight: 600; text-transform: uppercase; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid ${(props) => props.theme.secondary.text}; ` @@ -60,7 +91,7 @@ const Content = styled.div` font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; ` const Markdown = styled.div` - +line-height:20px; font-size: 12px; pre { padding: 8px; @@ -86,6 +117,7 @@ font-size: 12px; p { margin: 0px; line-height: 1.35rem; + font-size: 12px; } code:not(pre code) { @@ -102,20 +134,20 @@ font-size: 12px; overflow-wrap: break-word; word-break: break-all; } - - ul{ - padding:0px; - list-style-position: inside; + a{ + color: #007ee6; } ` export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "dark" + theme = "dark", + placeholder = "Search or Ask AI" }: SearchBarProps) => { const [input, setInput] = React.useState("") const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null) + const widgetRef = React.useRef(null) const [results, setResults] = React.useState([]) React.useEffect(() => { input.length > 0 ? @@ -125,6 +157,9 @@ export const SearchBar = ({ : setResults([]) }, [input]) + React.useEffect(()=>{ + console.log(isWidgetOpen, input); + },[isWidgetOpen]) const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -135,43 +170,44 @@ export const SearchBar = ({ } const md = new MarkdownIt(); return ( -
- - handleKeyDown(e)} - placeholder='Search here or Ask DocsGPT' - value={input} - onChange={(e) => setInput(e.target.value)} + +
+ + handleKeyDown(e)} + placeholder={placeholder} + value={input} + onChange={(e) => setInput(e.target.value)} + /> + { + input.length>0 && results.length > 0 && ( + + {results.map((res) => ( +
+ {res.title} + + + +
+ )) + } +
+ ) + } +
+ - { - results.length > 0 && ( - - {results.map((res) => ( -
- {res.title} - - - -
- )) - } -
- ) - } - - - -
+
+ ) } \ No newline at end of file diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 445260dc..f8fc3753 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -44,7 +44,8 @@ export interface WidgetCoreProps extends WidgetProps { export interface SearchBarProps { apiHost?: string; apiKey?: string; - theme?:THEME + theme?:THEME; + placeholder?:string; } export interface Result { From 7bd0351ee9ff12a4082726b2fb9be03a17094f65 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 06:25:24 +0530 Subject: [PATCH 04/21] (fix): mount only when open --- extensions/react-widget/package-lock.json | 9 +- .../src/components/DocsGPTWidget.tsx | 212 +++++++++--------- .../react-widget/src/components/SearchBar.tsx | 52 +++-- 3 files changed, 139 insertions(+), 134 deletions(-) diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index de4c228d..6d736c6f 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -4860,9 +4860,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "funding": [ { "type": "opencollective", @@ -4876,7 +4876,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 6fff4f46..7a43cb7b 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -65,8 +65,11 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` position: fixed; right: ${props => props.modal ? '50%' : '10px'}; bottom: ${props => props.modal ? '50%' : '10px'}; - z-index: 1000; + z-index: 1001; transform-origin:100% 100%; + &.modal{ + transform : translate(50%,50%); + } &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; display: block; @@ -75,9 +78,6 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; display: none; } - ${props => props.modal && - "transform : translate(50%,50%);" - } align-items: center; text-align: left; @keyframes createBox { @@ -539,7 +539,9 @@ export const WidgetCore = ({ prefilledQuery = "", handleClose }: WidgetCoreProps) => { - const [prompt, setPrompt] = React.useState(prefilledQuery); + const [prompt, setPrompt] = React.useState(prefilledQuery); + console.log("propmpt",prompt); + const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); const [conversationId, setConversationId] = React.useState(null); @@ -666,106 +668,110 @@ export const WidgetCore = ({ {isOpen && size === 'large' && } - - { -
- - - -
- docs-gpt - - {title} - {description} - -
-
- - { - queries.length > 0 ? queries?.map((query, index) => { - return ( - - { - query.prompt && - - {query.prompt} - - - } - { - query.response ? { isBubbleHovered.current = true }} type='ANSWER'> - - - + { + isOpen && ( + + +
+ + + +
+ docs-gpt + + {title} + {description} + +
+
+ + { + queries.length > 0 ? queries?.map((query, index) => { + return ( + + { + query.prompt && + + {query.prompt} + + + } + { + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + + - {collectFeedback && - - handleFeedback("LIKE", index)} /> - handleFeedback("DISLIKE", index)} /> - } - - :
- { - query.error ? + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } + + :
+ { + query.error ? - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- } - ) - }) - : - } - -
- - setPrompt(event.target.value)} - type='text' placeholder="Ask your question" /> - - - - - - Powered by  - DocsGPT - -
- } - + +
+
Network Error
+ {query.error} +
+
+ : + + . + . + . + + + } +
+ } +
) + }) + : + } +
+
+ + setPrompt(event.target.value)} + type='text' placeholder="Ask your question" /> + + + + + + Powered by  + DocsGPT + +
+
+
+ ) + } ) } \ No newline at end of file diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index a6a48173..2589cb2f 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -9,30 +9,30 @@ import DOMPurify from 'dompurify'; const themes = { dark: { - bg: '#222327', - text: '#fff', - primary: { - text: "#FAFAFA", - bg: '#111111' - }, - secondary: { - text: "#A1A1AA", - bg: "#38383b" - } + bg: '#222327', + text: '#fff', + primary: { + text: "#FAFAFA", + bg: '#111111' + }, + secondary: { + text: "#A1A1AA", + bg: "#38383b" + } }, light: { - bg: '#fff', - text: '#000', - primary: { - text: "#222327", - bg: "#fff" - }, - secondary: { - text: "#A1A1AA", - bg: "#F6F6F6" - } + bg: '#fff', + text: '#000', + primary: { + text: "#222327", + bg: "#fff" + }, + secondary: { + text: "#A1A1AA", + bg: "#F6F6F6" + } } - } +} const Main = styled.div` all:initial; @@ -157,9 +157,7 @@ export const SearchBar = ({ : setResults([]) }, [input]) - React.useEffect(()=>{ - console.log(isWidgetOpen, input); - },[isWidgetOpen]) + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -181,7 +179,7 @@ export const SearchBar = ({ onChange={(e) => setInput(e.target.value)} /> { - input.length>0 && results.length > 0 && ( + input.length > 0 && results.length > 0 && ( {results.map((res) => (
@@ -198,14 +196,14 @@ export const SearchBar = ({ ) } - + />} From d33246612dcc926f1a64a956ef1810feef1cab56 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 15 Nov 2024 18:16:45 +0530 Subject: [PATCH 05/21] (widget) unmount with timeout --- .../src/components/DocsGPTWidget.tsx | 246 +++++++++--------- .../react-widget/src/components/SearchBar.tsx | 18 +- frontend/.env.development | 2 +- 3 files changed, 137 insertions(+), 129 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 7a43cb7b..557aa9a0 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -67,16 +67,15 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` bottom: ${props => props.modal ? '50%' : '10px'}; z-index: 1001; transform-origin:100% 100%; + display: block; &.modal{ transform : translate(50%,50%); } &.open { animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - display: block; } &.close { animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - display: none; } align-items: center; text-align: left; @@ -494,24 +493,20 @@ export const DocsGPTWidget = (props: WidgetProps) => { const [open, setOpen] = React.useState(defaultOpen); const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); - const widgetRef = useRef(null) + React.useEffect(() => { + if(isFloatingButtonVisible) + setTimeout(() => setIsAnimatingButton(false), 400); + }, [isFloatingButtonVisible]) const handleClose = () => { + setIsFloatingButtonVisible(true); + setIsAnimatingButton(true); setOpen(false); - setTimeout(() => { - if (widgetRef.current) - widgetRef.current.style.display = "none"; - setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); - setTimeout(() => setIsAnimatingButton(false), 200); - }, 250) }; const handleOpen = () => { setOpen(true); setIsFloatingButtonVisible(false); - if (widgetRef.current) - widgetRef.current.style.display = 'block' } return ( <> @@ -519,7 +514,7 @@ export const DocsGPTWidget = (props: WidgetProps) => { {buttonText} - + ) } @@ -534,14 +529,13 @@ export const WidgetCore = ({ size = 'small', theme = 'dark', collectFeedback = true, - widgetRef = null, isOpen = false, prefilledQuery = "", handleClose }: WidgetCoreProps) => { - const [prompt, setPrompt] = React.useState(prefilledQuery); - console.log("propmpt",prompt); - + const [prompt, setPrompt] = React.useState(""); + console.log("propmpt", prompt); + const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); const [conversationId, setConversationId] = React.useState(null); @@ -551,6 +545,22 @@ export const WidgetCore = ({ const endMessageRef = React.useRef(null); const md = new MarkdownIt(); + React.useEffect(() => { + if (isOpen) { + setMounted(true); // Mount the component + setPrompt(prefilledQuery) + } else { + // Wait for animations before unmounting + const timeout = setTimeout(() => { + setMounted(false) + console.log("Unmounted syccess") + }, 250); + return () => clearTimeout(timeout); + } + }, [isOpen]); + + + const handleUserInterrupt = () => { (status === 'loading') && setEventInterrupt(true); } @@ -663,114 +673,114 @@ export const WidgetCore = ({ typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; + if (!mounted) return null; return ( {isOpen && size === 'large' && } - { - isOpen && ( - - -
- - - -
- docs-gpt - - {title} - {description} - -
-
- - { - queries.length > 0 ? queries?.map((query, index) => { - return ( - - { - query.prompt && - - {query.prompt} - - - } - { - query.response ? { isBubbleHovered.current = true }} type='ANSWER'> - - - + {( + + +
+ + + +
+ docs-gpt + + {title} + {description} + +
+
+ + { + queries.length > 0 ? queries?.map((query, index) => { + return ( + + { + query.prompt && + + {query.prompt} + + + } + { + query.response ? { isBubbleHovered.current = true }} type='ANSWER'> + + + - {collectFeedback && - - handleFeedback("LIKE", index)} /> - handleFeedback("DISLIKE", index)} /> - } - - :
- { - query.error ? + {collectFeedback && + + handleFeedback("LIKE", index)} /> + handleFeedback("DISLIKE", index)} /> + } + + :
+ { + query.error ? - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- } - ) - }) - : - } - -
- - setPrompt(event.target.value)} - type='text' placeholder="Ask your question" /> - - - - - - Powered by  - DocsGPT - -
- - - ) + +
+
Network Error
+ {query.error} +
+
+ : + + . + . + . + + + } +
+ } +
) + }) + : + } +
+
+ + setPrompt(event.target.value)} + type='text' placeholder="Ask your question" /> + + + + + + Powered by  + DocsGPT + +
+
+
+ ) }
) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 2589cb2f..5486871b 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -45,11 +45,12 @@ const TextField = styled.input` display: inline; color: ${props => props.theme.primary.text}; outline: none; - border: 2px solid transparent; + border: none; background-color: ${props => props.theme.secondary.bg}; + width: 240px; &:focus { - border:2px solid #007ee6; - box-shadow: 0px 0px 2px skyblue; + outline: none; + box-shadow: 0px 0px 0px 2px rgba(0, 109, 199); background-color: ${props => props.theme.primary.bg}; } ` @@ -84,9 +85,7 @@ const Title = styled.h3` font-weight: 600; text-transform: uppercase; border-bottom: 1px solid ${(props) => props.theme.secondary.text}; - ` - const Content = styled.div` font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; ` @@ -141,7 +140,7 @@ font-size: 12px; export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "dark", + theme = "light", placeholder = "Search or Ask AI" }: SearchBarProps) => { const [input, setInput] = React.useState("") @@ -157,7 +156,7 @@ export const SearchBar = ({ : setResults([]) }, [input]) - + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsWidgetOpen(true); @@ -196,15 +195,14 @@ export const SearchBar = ({ ) } - {isWidgetOpen && } - + /> ) diff --git a/frontend/.env.development b/frontend/.env.development index 7a87f762..9569966a 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1,3 @@ # Please put appropriate value -VITE_API_HOST=http://0.0.0.0:7091 +VITE_API_HOST=http://127.0.0.1:7091 VITE_API_STREAMING=true \ No newline at end of file From 94617c5ef72f126802f82de4a04402dd412440c9 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 16 Nov 2024 02:13:40 +0530 Subject: [PATCH 06/21] (fix:animations) minor fix --- .../src/components/DocsGPTWidget.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 557aa9a0..725e829a 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -105,10 +105,10 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` `; const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; - max-height: ${(props) => props.theme.dimensions.maxHeight} !important; - max-width: ${(props) => props.theme.dimensions.maxWidth} !important; - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + max-height: ${(props) => props.theme.dimensions.maxHeight}; + max-width: ${(props) => props.theme.dimensions.maxWidth}; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height} ; position: relative; flex-direction: column; justify-content: space-between; @@ -134,15 +134,15 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` height: 100px; } 100% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; border-radius: 12px; } } @keyframes closeContainer { 0% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; border-radius: 12px; } 100% { @@ -495,13 +495,15 @@ export const DocsGPTWidget = (props: WidgetProps) => { const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); React.useEffect(() => { - if(isFloatingButtonVisible) - setTimeout(() => setIsAnimatingButton(false), 400); + if (isFloatingButtonVisible) + setTimeout(() => setIsAnimatingButton(true), 250); + return () => { + setIsAnimatingButton(false) + } }, [isFloatingButtonVisible]) const handleClose = () => { setIsFloatingButtonVisible(true); - setIsAnimatingButton(true); setOpen(false); }; const handleOpen = () => { @@ -534,7 +536,6 @@ export const WidgetCore = ({ handleClose }: WidgetCoreProps) => { const [prompt, setPrompt] = React.useState(""); - console.log("propmpt", prompt); const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); @@ -553,7 +554,6 @@ export const WidgetCore = ({ // Wait for animations before unmounting const timeout = setTimeout(() => { setMounted(false) - console.log("Unmounted syccess") }, 250); return () => clearTimeout(timeout); } From 868e59bca00d01524b6dfb046b407d49411aa0af Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 17 Nov 2024 03:31:09 +0530 Subject: [PATCH 07/21] (fix: css conflict): replace global keyframes --- .../src/components/DocsGPTWidget.tsx | 180 +++++++++--------- 1 file changed, 91 insertions(+), 89 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 725e829a..d471aa57 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useRef } from 'react' import DOMPurify from 'dompurify'; -import styled, { keyframes } from 'styled-components'; +import styled, { keyframes,css } from 'styled-components'; import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; @@ -49,7 +49,86 @@ const sizesConfig = { maxHeight: custom.maxHeight || '70vh', }), }; +const createBox = keyframes` + 0% { + transform: scale(0.6); + } + 90% { + transform: scale(1.02); + } + 100% { + transform: scale(1); + } +` +const closeBox = keyframes` + 0% { + transform: scale(1); + } + 10% { + transform: scale(1.02); + } + 100% { + transform: scale(0.6); + } +` +const openContainer = keyframes` + 0% { + width: 200px; + height: 100px; + } + 100% { + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; + border-radius: 12px; + }` +const closeContainer = keyframes` + 0% { + width: ${(props) => props.theme.dimensions.width} !important; + height: ${(props) => props.theme.dimensions.height} !important; + border-radius: 12px; + } + 100% { + width: 200px; + height: 100px; + } +` +const fadeIn = keyframes` + from { + opacity: 0; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } +` + +const fadeOut = keyframes` + from { + opacity: 1; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } + to { + opacity: 0; + transform: scale(0.9); + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; + } +` +const scaleAnimation = keyframes` + from { + transform: scale(1.2); + } + to { + transform: scale(1); + } +` const Overlay = styled.div` position: fixed; top: 0; @@ -60,6 +139,8 @@ const Overlay = styled.div` z-index: 999; transition: opacity 0.5s; ` + + const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` all: initial; position: fixed; @@ -72,37 +153,15 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>` transform : translate(50%,50%); } &.open { - animation: createBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; } &.close { - animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; } align-items: center; text-align: left; - @keyframes createBox { - 0% { - transform: scale(0.6); - } - 90% { - transform: scale(1.02); - } - 100% { - transform: scale(1); - } - } - - @keyframes closeBox { - 0% { - transform: scale(1); - } - 10% { - transform: scale(1.02); - } - 100% { - transform: scale(0.6); - } - } `; + const StyledContainer = styled.div<{ isOpen: boolean }>` all: initial; max-height: ${(props) => props.theme.dimensions.maxHeight}; @@ -123,66 +182,18 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` animation: ${({ isOpen, theme }) => theme.dimensions.size === 'large' ? isOpen - ? 'fadeIn 150ms ease-in forwards' - : 'fadeOut 150ms ease-in forwards' + ? css `${fadeIn} 150ms ease-in forwards` + : css ` ${fadeOut} 150ms ease-in forwards` : isOpen - ? 'openContainer 150ms ease-in forwards' - : 'closeContainer 250ms ease-in forwards'}; - @keyframes openContainer { - 0% { - width: 200px; - height: 100px; - } - 100% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; - border-radius: 12px; - } - } - @keyframes closeContainer { - 0% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; - border-radius: 12px; - } - 100% { - width: 200px; - height: 100px; - } - } - @keyframes fadeIn { - from { - opacity: 0; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - } - @keyframes fadeOut { - from { - opacity: 1; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - to { - opacity: 0; - transform: scale(0.9); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; - } - } + ? css `${openContainer} 150ms ease-in forwards` + : css `${closeContainer} 250ms ease-in forwards`}; @media only screen and (max-width: 768px) { max-height: 100vh; max-width: 80vw; overflow: auto; } `; + const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatingButton: boolean }>` position: fixed; display: ${props => props.hidden ? "none" : "flex"}; @@ -200,7 +211,7 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatin background: ${props => props.bgcolor}; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); cursor: pointer; - animation: ${props => props.isAnimatingButton ? 'scaleAnimation 200ms forwards' : 'none'}; + animation: ${props => props.isAnimatingButton ? css `${scaleAnimation} 200ms forwards` : 'none'}; &:hover { transform: scale(1.1); transition: transform 0.2s ease-in-out; @@ -208,15 +219,6 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatin &:not(:hover) { transition: transform 0.2s ease-in-out; } - - @keyframes scaleAnimation { - from { - transform: scale(1.2); - } - to { - transform: scale(1); - } - } `; const CancelButton = styled.button` cursor: pointer; From 5ee0f15d946ea04de0433d0a9322f3c10343e96b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 18 Nov 2024 03:18:39 +0530 Subject: [PATCH 08/21] (feat: search): close on click outside --- .../src/components/DocsGPTWidget.tsx | 9 +-- .../react-widget/src/components/SearchBar.tsx | 58 +++++++++++++++---- extensions/react-widget/src/types/index.ts | 1 + frontend/.env.development | 2 +- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index d471aa57..b9ee8acc 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -78,14 +78,14 @@ const openContainer = keyframes` height: 100px; } 100% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; border-radius: 12px; }` const closeContainer = keyframes` 0% { - width: ${(props) => props.theme.dimensions.width} !important; - height: ${(props) => props.theme.dimensions.height} !important; + width: ${(props) => props.theme.dimensions.width}; + height: ${(props) => props.theme.dimensions.height}; border-radius: 12px; } 100% { @@ -671,6 +671,7 @@ export const WidgetCore = ({ const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; + const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 5486871b..a417b955 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -39,15 +39,16 @@ const Main = styled.div` font-family: sans-serif; ` -const TextField = styled.input` +const TextField = styled.input<{inputWidth:string}>` padding: 6px 6px; + width: ${({inputWidth}) => inputWidth}; border-radius: 8px; display: inline; color: ${props => props.theme.primary.text}; outline: none; border: none; background-color: ${props => props.theme.secondary.bg}; - width: 240px; + &:focus { outline: none; box-shadow: 0px 0px 0px 2px rgba(0, 109, 199); @@ -61,6 +62,7 @@ const Container = styled.div` ` const SearchResults = styled.div` position: absolute; + display: block; background-color: ${props => props.theme.primary.bg}; opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); @@ -77,6 +79,11 @@ const SearchResults = styled.div` 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; + } ` const Title = styled.h3` font-size: 14px; @@ -137,17 +144,45 @@ font-size: 12px; color: #007ee6; } ` +const Toolkit = styled.kbd` + position: absolute; + right: 12px; + top: 4px; + background-color: ${(props) => props.theme.primary.bg}; + color: ${(props) => props.theme.secondary.text}; + font-size: 9px; + padding: 2px; + border: 1px solid ${(props) => props.theme.secondary.text}; + border-radius: 4px; +` export const SearchBar = ({ apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", apiHost = "http://127.0.0.1:7091", - theme = "light", - placeholder = "Search or Ask AI" + theme = "dark", + placeholder = "Search or Ask AI...", + width="240px" }: SearchBarProps) => { - const [input, setInput] = React.useState("") + const [input, setInput] = React.useState(""); const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); - const inputRef = React.useRef(null) - const widgetRef = React.useRef(null) - const [results, setResults] = React.useState([]) + const inputRef = React.useRef(null); + const resultsRef = React.useRef(null); + const [isResultVisible, setIsResultVisible] = React.useState(true); + const [results, setResults] = React.useState([]); + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + resultsRef.current && + !resultsRef.current.contains(event.target as Node) + ) { + setIsResultVisible(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + resultsRef.current && (resultsRef.current.style.display = 'block') + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []) React.useEffect(() => { input.length > 0 ? getSearchResults(input, apiKey, apiHost) @@ -171,6 +206,8 @@ export const SearchBar = ({
setIsResultVisible(true)} ref={inputRef} onKeyDown={(e) => handleKeyDown(e)} placeholder={placeholder} @@ -178,8 +215,8 @@ export const SearchBar = ({ onChange={(e) => setInput(e.target.value)} /> { - input.length > 0 && results.length > 0 && ( - + input.length > 0 && results.length > 0 && isResultVisible && ( + {results.map((res) => (
{res.title} @@ -194,6 +231,7 @@ export const SearchBar = ({ ) } + Enter Date: Mon, 18 Nov 2024 17:00:56 +0530 Subject: [PATCH 09/21] (search): modal triggers on ctrl k --- extensions/react-widget/src/components/SearchBar.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index a417b955..f953744d 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -150,8 +150,8 @@ const Toolkit = styled.kbd` top: 4px; background-color: ${(props) => props.theme.primary.bg}; color: ${(props) => props.theme.secondary.text}; - font-size: 9px; - padding: 2px; + font-size: 10px; + padding: 3px; border: 1px solid ${(props) => props.theme.secondary.text}; border-radius: 4px; ` @@ -193,7 +193,8 @@ export const SearchBar = ({ }, [input]) const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { + event.preventDefault(); + if (event.ctrlKey && event.key === 'k'){ setIsWidgetOpen(true); } }; @@ -231,7 +232,7 @@ export const SearchBar = ({ ) } - Enter + Ctrl K Date: Mon, 18 Nov 2024 19:06:41 +0530 Subject: [PATCH 10/21] (feat/search): redirect to sources --- .../react-widget/src/components/SearchBar.tsx | 64 +++++++++++++------ extensions/react-widget/src/types/index.ts | 3 +- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index f953744d..d1d3aa1a 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -39,9 +39,9 @@ const Main = styled.div` font-family: sans-serif; ` -const TextField = styled.input<{inputWidth:string}>` +const TextField = styled.input<{ inputWidth: string }>` padding: 6px 6px; - width: ${({inputWidth}) => inputWidth}; + width: ${({ inputWidth }) => inputWidth}; border-radius: 8px; display: inline; color: ${props => props.theme.primary.text}; @@ -67,7 +67,7 @@ const SearchResults = styled.div` opacity: 90%; border: 1px solid rgba(0, 0, 0, .1); border-radius: 12px; - padding: 15px; + padding: 8px; width: 576px; z-index: 100; height: 25vh; @@ -87,7 +87,8 @@ const SearchResults = styled.div` ` const Title = styled.h3` font-size: 14px; - color: rgb(107, 114, 128); + color: ${props => props.theme.primary.text}; + opacity: 0.8; padding-bottom: 6px; font-weight: 600; text-transform: uppercase; @@ -96,6 +97,17 @@ const Title = styled.h3` const Content = styled.div` font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; ` +const ResultWrapper = styled.div` + padding: 4px 8px 4px 8px; + border-radius: 8px; + cursor: pointer; + &.contains-source:hover{ + background-color: rgba(0, 92, 197, 0.15); + ${Title} { + color: rgb(0, 126, 230); + } + } +` const Markdown = styled.div` line-height:20px; font-size: 12px; @@ -112,7 +124,8 @@ font-size: 12px; h1,h2 { font-size: 16px; font-weight: 600; - color: rgb(31, 41, 55); + color: ${(props) => props.theme.text}; + opacity: 0.8; } @@ -150,17 +163,18 @@ const Toolkit = styled.kbd` top: 4px; background-color: ${(props) => props.theme.primary.bg}; color: ${(props) => props.theme.secondary.text}; + font-weight: 600; font-size: 10px; padding: 3px; border: 1px solid ${(props) => props.theme.secondary.text}; border-radius: 4px; ` export const SearchBar = ({ - apiKey = "79bcbf0e-3dd1-4ac3-b893-e41b3d40ec8d", - apiHost = "http://127.0.0.1:7091", + apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", + apiHost = "https://gptcloud.arc53.com", theme = "dark", placeholder = "Search or Ask AI...", - width="240px" + width = "240px" }: SearchBarProps) => { const [input, setInput] = React.useState(""); const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); @@ -193,8 +207,8 @@ export const SearchBar = ({ }, [input]) const handleKeyDown = (event: React.KeyboardEvent) => { - event.preventDefault(); - if (event.ctrlKey && event.key === 'k'){ + if (event.ctrlKey && event.key === 'k') { + event.preventDefault(); setIsWidgetOpen(true); } }; @@ -208,7 +222,7 @@ export const SearchBar = ({ setIsResultVisible(true)} + onFocus={() => setIsResultVisible(true)} ref={inputRef} onKeyDown={(e) => handleKeyDown(e)} placeholder={placeholder} @@ -218,16 +232,24 @@ export const SearchBar = ({ { input.length > 0 && results.length > 0 && isResultVisible && ( - {results.map((res) => ( -
- {res.title} - - - -
- )) + {results.map((res) => { + const containsSource = res.source !== 'local' + return ( + { + if (!containsSource) return; + window.open(res.source, '_blank', 'noopener, noreferrer') + }} + className={containsSource ? "contains-source" : ""}> + {res.title} + + + + + ) + }) }
) diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index abab9f9c..cea9e43a 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -51,5 +51,6 @@ export interface SearchBarProps { export interface Result { text:string; - title:string + title:string; + source:string; } From 8641a911824d7a3337f8d2e4beaae8f4a15aadf5 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 19 Nov 2024 03:38:13 +0530 Subject: [PATCH 11/21] (feat-search): adding loader, no-results --- .../react-widget/src/components/SearchBar.tsx | 94 ++++++++++++++----- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index d1d3aa1a..f4e55a58 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -48,11 +48,14 @@ const TextField = styled.input<{ inputWidth: string }>` outline: none; border: none; background-color: ${props => props.theme.secondary.bg}; - + transition: background-color 128ms linear; &:focus { - outline: none; - box-shadow: 0px 0px 0px 2px rgba(0, 109, 199); - background-color: ${props => props.theme.primary.bg}; + outline: none; + box-shadow: + rgb(0, 109, 199) 0px 0px 3px, + rgb(0, 90, 163) 0px 0px 6px, + rgba(0, 0, 0, 0.25) 0px 2px 6px; + background-color: ${props => props.theme.primary.bg}; } ` @@ -69,6 +72,7 @@ const SearchResults = styled.div` border-radius: 12px; padding: 8px; width: 576px; + min-width: 96%; z-index: 100; height: 25vh; overflow-y: auto; @@ -169,6 +173,31 @@ const Toolkit = styled.kbd` border: 1px solid ${(props) => props.theme.secondary.text}; border-radius: 4px; ` +const Loader = styled.div` + margin: 2rem auto; + border: 4px solid #f3f3f3; + border-top: 4px solid ${props => props.theme.primary.bg}; + border-radius: 50%; + width: 12px; + height: 12px; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; + +const NoResults = styled.div` + margin-top: 2rem; + text-align: center; + font-size: 1rem; + color: #888; +`; export const SearchBar = ({ apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", apiHost = "https://gptcloud.arc53.com", @@ -177,6 +206,7 @@ export const SearchBar = ({ width = "240px" }: SearchBarProps) => { const [input, setInput] = React.useState(""); + const [loading, setLoading] = React.useState(false) const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null); const resultsRef = React.useRef(null); @@ -199,9 +229,15 @@ export const SearchBar = ({ }, []) React.useEffect(() => { input.length > 0 ? - getSearchResults(input, apiKey, apiHost) - .then((data) => setResults(data)) + (() => { + setLoading(true) + getSearchResults(input, apiKey, apiHost) + .then((data) => { + setResults(data) + setLoading(false) + }) .catch((err) => console.log(err)) + })() : setResults([]) }, [input]) @@ -221,6 +257,7 @@ export const SearchBar = ({
setIsResultVisible(true)} ref={inputRef} @@ -230,26 +267,33 @@ export const SearchBar = ({ onChange={(e) => setInput(e.target.value)} /> { - input.length > 0 && results.length > 0 && isResultVisible && ( + input.length > 0 && isResultVisible && ( - {results.map((res) => { - const containsSource = res.source !== 'local' - return ( - { - if (!containsSource) return; - window.open(res.source, '_blank', 'noopener, noreferrer') - }} - className={containsSource ? "contains-source" : ""}> - {res.title} - - - - + {!loading ? + (results.length > 0 ? + results.map((res) => { + const containsSource = res.source !== 'local'; + return ( + { + if (!containsSource) return; + window.open(res.source, '_blank', 'noopener, noreferrer') + }} + className={containsSource ? "contains-source" : ""}> + {res.title} + + + + + ) + }) + : + No results ) - }) + : + } ) @@ -262,7 +306,7 @@ export const SearchBar = ({ apiKey={apiKey} prefilledQuery={input} isOpen={isWidgetOpen} - handleClose={handleClose} size={'large'} + handleClose={handleClose} size={"large"} />
From 91239820e3c78f3451d140869caf07be13de5a6f Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 19 Nov 2024 04:25:54 +0530 Subject: [PATCH 12/21] (feat/search): debounce and abort previous pending req --- .../react-widget/src/components/SearchBar.tsx | 41 ++++++++---- .../react-widget/src/requests/searchAPI.ts | 63 ++++++++++--------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index f4e55a58..8cf30632 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -36,7 +36,6 @@ const themes = { const Main = styled.div` all:initial; - font-family: sans-serif; ` const TextField = styled.input<{ inputWidth: string }>` @@ -206,12 +205,14 @@ export const SearchBar = ({ width = "240px" }: SearchBarProps) => { const [input, setInput] = React.useState(""); - const [loading, setLoading] = React.useState(false) + const [loading, setLoading] = React.useState(false); const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null); const resultsRef = React.useRef(null); const [isResultVisible, setIsResultVisible] = React.useState(true); const [results, setResults] = React.useState([]); + const debounceTimeout = React.useRef(null); + const abortControllerRef = React.useRef(null) React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( @@ -228,18 +229,32 @@ export const SearchBar = ({ }; }, []) React.useEffect(() => { - input.length > 0 ? - (() => { - setLoading(true) - getSearchResults(input, apiKey, apiHost) - .then((data) => { - setResults(data) - setLoading(false) - }) + if (!input) { + setResults([]); + return; + } + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + debounceTimeout.current = setTimeout(() => { + setLoading(true); + getSearchResults(input, apiKey, apiHost, abortController.signal) + .then((data) => setResults(data)) .catch((err) => console.log(err)) - })() - : - setResults([]) + .finally(() => setLoading(false)); + }, 500); + + return () => { + abortController.abort(); + clearTimeout(debounceTimeout.current ?? undefined); + }; }, [input]) const handleKeyDown = (event: React.KeyboardEvent) => { diff --git a/extensions/react-widget/src/requests/searchAPI.ts b/extensions/react-widget/src/requests/searchAPI.ts index 18df3691..7411a810 100644 --- a/extensions/react-widget/src/requests/searchAPI.ts +++ b/extensions/react-widget/src/requests/searchAPI.ts @@ -1,34 +1,37 @@ import { Result } from "@/types"; - async function getSearchResults(question: string, apiKey:string, apiHost:string): Promise { - - const payload = { - question, - api_key:apiKey - }; - - try { - const response = await fetch(`${apiHost}/api/search`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.status}`); - } - - const data: Result[] = await response.json(); - return data; - - } catch (error) { - console.error("Failed to fetch documents:", error); - throw error; +async function getSearchResults(question: string, apiKey: string, apiHost: string, signal: AbortSignal): Promise { + + const payload = { + question, + api_key: apiKey + }; + + try { + const response = await fetch(`${apiHost}/api/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + signal: signal + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status}`); } + + const data: Result[] = await response.json(); + return data; + + } catch (error) { + if (!(error instanceof DOMException && error.name == "AbortError")) { + console.error("Failed to fetch documents:", error); + } + throw error; } - - export { - getSearchResults - } \ No newline at end of file +} + +export { + getSearchResults +} \ No newline at end of file From e0a3b8004c51ef865ac95b3d552ec979b2a25a77 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 19 Nov 2024 04:41:12 +0530 Subject: [PATCH 13/21] (fix): minor ui; loading state --- extensions/react-widget/src/components/SearchBar.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 8cf30632..e38b381c 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -51,9 +51,9 @@ const TextField = styled.input<{ inputWidth: string }>` &:focus { outline: none; box-shadow: - rgb(0, 109, 199) 0px 0px 3px, - rgb(0, 90, 163) 0px 0px 6px, - rgba(0, 0, 0, 0.25) 0px 2px 6px; + 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}; } ` @@ -174,7 +174,7 @@ const Toolkit = styled.kbd` ` const Loader = styled.div` margin: 2rem auto; - border: 4px solid #f3f3f3; + border: 4px solid ${props => props.theme.secondary.text}; border-top: 4px solid ${props => props.theme.primary.bg}; border-radius: 50%; width: 12px; @@ -233,6 +233,7 @@ export const SearchBar = ({ setResults([]); return; } + setLoading(true); if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); } @@ -244,7 +245,6 @@ export const SearchBar = ({ abortControllerRef.current = abortController; debounceTimeout.current = setTimeout(() => { - setLoading(true); getSearchResults(input, apiKey, apiHost, abortController.signal) .then((data) => setResults(data)) .catch((err) => console.log(err)) From 312cb9ae7046d82e1d43d4ee835be8b1fc56b9bd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 19 Nov 2024 19:06:53 +0000 Subject: [PATCH 14/21] feat: image parser --- application/api/user/routes.py | 6 ++++++ application/core/settings.py | 1 + application/parser/file/bulk.py | 4 ++++ application/parser/file/docs_parser.py | 12 ++++++++++- application/parser/file/image_parser.py | 28 +++++++++++++++++++++++++ frontend/src/upload/Upload.tsx | 3 +++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 application/parser/file/image_parser.py diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 6a2f3bea..e305845d 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -339,6 +339,9 @@ class UploadFile(Resource): ".json", ".xlsx", ".pptx", + ".png", + ".jpg", + ".jpeg", ], job_name, final_filename, @@ -365,6 +368,9 @@ class UploadFile(Resource): ".json", ".xlsx", ".pptx", + ".png", + ".jpg", + ".jpeg", ], job_name, final_filename, diff --git a/application/core/settings.py b/application/core/settings.py index d4b02481..a7811ec7 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -18,6 +18,7 @@ class Settings(BaseSettings): DEFAULT_MAX_HISTORY: int = 150 MODEL_TOKEN_LIMITS: dict = {"gpt-3.5-turbo": 4096, "claude-2": 1e5} UPLOAD_FOLDER: str = "inputs" + PARSE_PDF_AS_IMAGE: bool = False VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search diff --git a/application/parser/file/bulk.py b/application/parser/file/bulk.py index 3b8fbca8..8201b3f2 100644 --- a/application/parser/file/bulk.py +++ b/application/parser/file/bulk.py @@ -13,6 +13,7 @@ from application.parser.file.rst_parser import RstParser from application.parser.file.tabular_parser import PandasCSVParser,ExcelParser from application.parser.file.json_parser import JSONParser from application.parser.file.pptx_parser import PPTXParser +from application.parser.file.image_parser import ImageParser from application.parser.schema.base import Document DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = { @@ -27,6 +28,9 @@ DEFAULT_FILE_EXTRACTOR: Dict[str, BaseParser] = { ".mdx": MarkdownParser(), ".json":JSONParser(), ".pptx":PPTXParser(), + ".png": ImageParser(), + ".jpg": ImageParser(), + ".jpeg": ImageParser(), } diff --git a/application/parser/file/docs_parser.py b/application/parser/file/docs_parser.py index 861e8e58..55d45a64 100644 --- a/application/parser/file/docs_parser.py +++ b/application/parser/file/docs_parser.py @@ -7,7 +7,8 @@ from pathlib import Path from typing import Dict from application.parser.file.base_parser import BaseParser - +from application.core.settings import settings +import requests class PDFParser(BaseParser): """PDF parser.""" @@ -18,6 +19,15 @@ class PDFParser(BaseParser): def parse_file(self, file: Path, errors: str = "ignore") -> str: """Parse file.""" + if settings.PARSE_PDF_AS_IMAGE: + doc2md_service = "https://llm.arc53.com/doc2md" + # alternatively you can use local vision capable LLM + with open(file, "rb") as file_loaded: + files = {'file': file_loaded} + response = requests.post(doc2md_service, files=files) + data = response.json()["markdown"] + return data + try: import PyPDF2 except ImportError: diff --git a/application/parser/file/image_parser.py b/application/parser/file/image_parser.py new file mode 100644 index 00000000..508693d8 --- /dev/null +++ b/application/parser/file/image_parser.py @@ -0,0 +1,28 @@ +"""Image parser. + +Contains parser for .png, .jpg, .jpeg files. + +""" +from pathlib import Path +import requests +from typing import Dict, Union +import traceback + +from application.parser.file.base_parser import BaseParser + + +class ImageParser(BaseParser): + """Image parser.""" + + def _init_parser(self) -> Dict: + """Init parser.""" + return {} + + def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]: + doc2md_service = "https://llm.arc53.com/doc2md" + # alternatively you can use local vision capable LLM + with open(file, "rb") as file_loaded: + files = {'file': file_loaded} + response = requests.post(doc2md_service, files=files) + data = response.json()["markdown"] + return data diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 2f28042a..33a77ace 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -332,6 +332,9 @@ function Upload({ ], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], + 'image/png': ['.png'], + 'image/jpeg': ['.jpeg'], + 'image/jpg': ['.jpg'], }, }); From f65ecb9a0f116bc926637f1832c911ea2022d551 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 19 Nov 2024 19:16:24 +0000 Subject: [PATCH 15/21] fix: lint import --- application/parser/file/image_parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/application/parser/file/image_parser.py b/application/parser/file/image_parser.py index 508693d8..fd800d91 100644 --- a/application/parser/file/image_parser.py +++ b/application/parser/file/image_parser.py @@ -6,7 +6,6 @@ Contains parser for .png, .jpg, .jpeg files. from pathlib import Path import requests from typing import Dict, Union -import traceback from application.parser.file.base_parser import BaseParser From 48f47351ee064ead60e895b7d4b031b09d472ed2 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Wed, 20 Nov 2024 17:04:45 +0530 Subject: [PATCH 16/21] (feat:keydown): listen cross platform events --- extensions/react-widget/package.json | 1 + .../react-widget/src/components/SearchBar.tsx | 46 ++++++++++++++++--- extensions/react-widget/src/utils/helper.ts | 27 +++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 extensions/react-widget/src/utils/helper.ts diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 554b2438..12a66939 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -32,6 +32,7 @@ "scripts": { "build": "parcel build src/main.tsx --public-url ./", "build:react": "parcel build src/index.ts", + "serve": "parcel serve -p 3000", "dev": "parcel -p 3000", "test": "jest", "lint": "eslint", diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index e38b381c..07f29f26 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -6,10 +6,10 @@ import { getSearchResults } from '../requests/searchAPI' import { Result } from '@/types'; import MarkdownIt from 'markdown-it'; import DOMPurify from 'dompurify'; - +import { getOS } from '../utils/helper' const themes = { dark: { - bg: '#222327', + bg: '#000', text: '#fff', primary: { text: "#FAFAFA", @@ -47,6 +47,9 @@ const TextField = styled.input<{ inputWidth: string }>` outline: none; border: none; background-color: ${props => props.theme.secondary.bg}; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; transition: background-color 128ms linear; &:focus { outline: none; @@ -114,6 +117,7 @@ const ResultWrapper = styled.div` const Markdown = styled.div` line-height:20px; font-size: 12px; +word-break: break-all; pre { padding: 8px; width: 90%; @@ -197,12 +201,25 @@ const NoResults = styled.div` font-size: 1rem; color: #888; `; +const InfoButton = styled.button` + padding: 10px 4px 10px 4px; + display: block; + width: 100%; + color: inherit; + border-radius: 6px; + background-color: ${(props) => props.theme.bg}; + text-align: center; + font-size: 14px; + margin-bottom: 8px; + border:1px solid ${(props) => props.theme.secondary.text}; + +` export const SearchBar = ({ apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", apiHost = "https://gptcloud.arc53.com", theme = "dark", placeholder = "Search or Ask AI...", - width = "240px" + width = "256px" }: SearchBarProps) => { const [input, setInput] = React.useState(""); const [loading, setLoading] = React.useState(false); @@ -213,6 +230,14 @@ export const SearchBar = ({ const [results, setResults] = React.useState([]); const debounceTimeout = React.useRef(null); const abortControllerRef = React.useRef(null) + const browserOS = getOS(); + const getKeyboardInstruction = () => { + if (browserOS === 'linux' || browserOS === 'win') + return "Ctrl K" + else if (browserOS === 'mac') + return "⌘ K" + else return "Enter" + } React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( @@ -258,7 +283,11 @@ export const SearchBar = ({ }, [input]) const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.ctrlKey && event.key === 'k') { + if ( + ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || + (browserOS === 'mac' && event.metaKey && event.key === 'k') || + ((browserOS === 'android' || browserOS === 'ios' || browserOS === 'other') && event.key === 'Enter') + ) { event.preventDefault(); setIsWidgetOpen(true); } @@ -272,10 +301,12 @@ export const SearchBar = ({
setIsResultVisible(true)} ref={inputRef} + onSubmit={() => setIsWidgetOpen(true)} onKeyDown={(e) => handleKeyDown(e)} placeholder={placeholder} value={input} @@ -284,6 +315,7 @@ export const SearchBar = ({ { input.length > 0 && isResultVisible && ( + {`Press ${getKeyboardInstruction()} to Ask the AI`} {!loading ? (results.length > 0 ? results.map((res) => { @@ -298,7 +330,7 @@ export const SearchBar = ({ {res.title} @@ -313,7 +345,9 @@ export const SearchBar = ({ ) } - Ctrl K + + {getKeyboardInstruction()} + { + const platform = window.navigator.platform; + const userAgent = window.navigator.userAgent || window.navigator.vendor; + + if (/Mac/i.test(platform)) { + return 'mac'; + } + + if (/Win/i.test(platform)) { + return 'win'; + } + + if (/Linux/i.test(platform) && !/Android/i.test(userAgent)) { + return 'linux'; + } + + if (/Android/i.test(userAgent)) { + return 'android'; + } + + if (/iPhone|iPad|iPod/i.test(userAgent)) { + return 'ios'; + } + + return 'other'; + }; + \ No newline at end of file From e9a8161811bd9b4799e02e7136aa5246fb9eefe5 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Wed, 20 Nov 2024 17:11:33 +0530 Subject: [PATCH 17/21] (feat/widget): autofocus input --- extensions/react-widget/src/components/DocsGPTWidget.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index b9ee8acc..afca895f 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -769,6 +769,7 @@ export const WidgetCore = ({ setPrompt(event.target.value)} type='text' placeholder="Ask your question" /> Date: Thu, 21 Nov 2024 23:54:19 +0530 Subject: [PATCH 18/21] (feat:widget) append prefilledQuery --- .../src/components/DocsGPTWidget.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index afca895f..d6273eaa 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useRef } from 'react' import DOMPurify from 'dompurify'; -import styled, { keyframes,css } from 'styled-components'; +import styled, { keyframes, css } from 'styled-components'; import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; @@ -182,11 +182,11 @@ const StyledContainer = styled.div<{ isOpen: boolean }>` animation: ${({ isOpen, theme }) => theme.dimensions.size === 'large' ? isOpen - ? css `${fadeIn} 150ms ease-in forwards` - : css ` ${fadeOut} 150ms ease-in forwards` + ? css`${fadeIn} 150ms ease-in forwards` + : css` ${fadeOut} 150ms ease-in forwards` : isOpen - ? css `${openContainer} 150ms ease-in forwards` - : css `${closeContainer} 250ms ease-in forwards`}; + ? css`${openContainer} 150ms ease-in forwards` + : css`${closeContainer} 250ms ease-in forwards`}; @media only screen and (max-width: 768px) { max-height: 100vh; max-width: 80vw; @@ -211,7 +211,7 @@ const FloatingButton = styled.div<{ bgcolor: string, hidden: boolean, isAnimatin background: ${props => props.bgcolor}; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); cursor: pointer; - animation: ${props => props.isAnimatingButton ? css `${scaleAnimation} 200ms forwards` : 'none'}; + animation: ${props => props.isAnimatingButton ? css`${scaleAnimation} 200ms forwards` : 'none'}; &:hover { transform: scale(1.1); transition: transform 0.2s ease-in-out; @@ -551,7 +551,7 @@ export const WidgetCore = ({ React.useEffect(() => { if (isOpen) { setMounted(true); // Mount the component - setPrompt(prefilledQuery) + appendQuery(prefilledQuery) } else { // Wait for animations before unmounting const timeout = setTimeout(() => { @@ -662,16 +662,24 @@ export const WidgetCore = ({ } // submit handler const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); + await appendQuery(prompt) + } + + const appendQuery = async (userQuery:string) => { + console.log(userQuery) + if(!userQuery) + return; + setEventInterrupt(false); - queries.push({ prompt }) - setPrompt('') - await stream(prompt) + queries.push({ prompt:userQuery}); + setPrompt(''); + await stream(userQuery); } const handleImageError = (event: React.SyntheticEvent) => { event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; }; - + const dimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) From c855896221732915f24d30e7dc5cb5eb0c97f78e Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 22 Nov 2024 00:03:44 +0530 Subject: [PATCH 19/21] (fix:events): enter on pc, tap on mobile --- .../react-widget/src/components/SearchBar.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 07f29f26..b4626724 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -166,7 +166,7 @@ word-break: break-all; ` const Toolkit = styled.kbd` position: absolute; - right: 12px; + right: 4px; top: 4px; background-color: ${(props) => props.theme.primary.bg}; color: ${(props) => props.theme.secondary.text}; @@ -202,6 +202,7 @@ const NoResults = styled.div` color: #888; `; const InfoButton = styled.button` + cursor: pointer; padding: 10px 4px 10px 4px; display: block; width: 100%; @@ -228,17 +229,24 @@ export const SearchBar = ({ const resultsRef = React.useRef(null); const [isResultVisible, setIsResultVisible] = React.useState(true); const [results, setResults] = React.useState([]); - const debounceTimeout = React.useRef(null); + const debounceTimeout = React.useRef | null>(null); const abortControllerRef = React.useRef(null) const browserOS = getOS(); - const getKeyboardInstruction = () => { - if (browserOS === 'linux' || browserOS === 'win') - return "Ctrl K" - else if (browserOS === 'mac') - return "⌘ K" - else return "Enter" + function isTouchDevice() { + return 'ontouchstart' in window; } + const isTouch = isTouchDevice(); + React.useEffect(() => { + const handleFocusSearch = (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 ( resultsRef.current && @@ -248,6 +256,7 @@ export const SearchBar = ({ } }; document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleFocusSearch); return () => { resultsRef.current && (resultsRef.current.style.display = 'block') document.removeEventListener('mousedown', handleClickOutside); @@ -283,15 +292,15 @@ export const SearchBar = ({ }, [input]) const handleKeyDown = (event: React.KeyboardEvent) => { - if ( - ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || - (browserOS === 'mac' && event.metaKey && event.key === 'k') || - ((browserOS === 'android' || browserOS === 'ios' || browserOS === 'other') && event.key === 'Enter') - ) { + if (event.key === 'Enter') { event.preventDefault(); - setIsWidgetOpen(true); + openWidget(); } }; + const openWidget = () => { + setIsWidgetOpen(true); + setIsResultVisible(false) + } const handleClose = () => { setIsWidgetOpen(false); } @@ -301,7 +310,6 @@ export const SearchBar = ({
setIsResultVisible(true)} @@ -315,7 +323,15 @@ export const SearchBar = ({ { input.length > 0 && isResultVisible && ( - {`Press ${getKeyboardInstruction()} to Ask the AI`} + + { + isTouch ? + "Ask the AI": + <> + Press Enter to ask the AI + + } + {!loading ? (results.length > 0 ? results.map((res) => { @@ -345,8 +361,10 @@ export const SearchBar = ({ ) } - - {getKeyboardInstruction()} + { + setIsWidgetOpen(true) + }} title={`${isTouch ? "Tap" : "Press Enter"} to Ask the AI`}> + {isTouch ? "Tap" : "Enter"} Date: Fri, 22 Nov 2024 17:30:50 +0530 Subject: [PATCH 20/21] (fix:search) spacing, minor bug --- .../react-widget/src/components/SearchBar.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index b4626724..7fd44c73 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -78,7 +78,7 @@ const SearchResults = styled.div` z-index: 100; height: 25vh; overflow-y: auto; - top: 45px; + top: 32px; color: ${props => props.theme.primary.text}; scrollbar-color: lab(48.438 0 0 / 0.4) rgba(0, 0, 0, 0); scrollbar-gutter: stable; @@ -226,7 +226,7 @@ export const SearchBar = ({ const [loading, setLoading] = React.useState(false); const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); const inputRef = React.useRef(null); - const resultsRef = React.useRef(null); + const containerRef = React.useRef(null); const [isResultVisible, setIsResultVisible] = React.useState(true); const [results, setResults] = React.useState([]); const debounceTimeout = React.useRef | null>(null); @@ -249,8 +249,8 @@ export const SearchBar = ({ } const handleClickOutside = (event: MouseEvent) => { if ( - resultsRef.current && - !resultsRef.current.contains(event.target as Node) + containerRef.current && + !containerRef.current.contains(event.target as Node) ) { setIsResultVisible(false); } @@ -258,7 +258,7 @@ export const SearchBar = ({ document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleFocusSearch); return () => { - resultsRef.current && (resultsRef.current.style.display = 'block') + setIsResultVisible(true); document.removeEventListener('mousedown', handleClickOutside); }; }, []) @@ -308,7 +308,7 @@ export const SearchBar = ({ return (
- + { input.length > 0 && isResultVisible && ( - + { - isTouch ? - "Ask the AI": - <> - Press Enter to ask the AI - + isTouch ? + "Ask the AI" : + <> + Press Enter to ask the AI + } {!loading ? From 1595e0210a7911f12d425a02c834a0d7a2b26a7b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 22 Nov 2024 17:55:37 +0530 Subject: [PATCH 21/21] (fix:search) change toolkit info --- .../react-widget/src/components/SearchBar.tsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index 7fd44c73..aff036d0 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -236,7 +236,13 @@ export const SearchBar = ({ return 'ontouchstart' in window; } const isTouch = isTouchDevice(); - + const getKeyboardInstruction = () => { + if (isResultVisible) return "Enter" + if (browserOS === 'mac') + return "⌘ K" + else + return "Ctrl K" + } React.useEffect(() => { const handleFocusSearch = (event: KeyboardEvent) => { if ( @@ -361,11 +367,22 @@ export const SearchBar = ({ ) } - { - setIsWidgetOpen(true) - }} title={`${isTouch ? "Tap" : "Press Enter"} to Ask the AI`}> - {isTouch ? "Tap" : "Enter"} - + { + isTouch ? + + { + setIsWidgetOpen(true) + }} + title={"Tap to Ask the AI"}> + Tap + + : + + {getKeyboardInstruction()} + + }