diff --git a/application/api/user/routes.py b/application/api/user/routes.py
index 6a2f3bea3..e305845d4 100644
--- a/application/api/user/routes.py
+++ b/application/api/user/routes.py
@@ -339,6 +339,9 @@ def post(self):
".json",
".xlsx",
".pptx",
+ ".png",
+ ".jpg",
+ ".jpeg",
],
job_name,
final_filename,
@@ -365,6 +368,9 @@ def post(self):
".json",
".xlsx",
".pptx",
+ ".png",
+ ".jpg",
+ ".jpeg",
],
job_name,
final_filename,
diff --git a/application/core/settings.py b/application/core/settings.py
index d4b02481c..a7811ec78 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 3b8fbca86..8201b3f22 100644
--- a/application/parser/file/bulk.py
+++ b/application/parser/file/bulk.py
@@ -13,6 +13,7 @@
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 @@
".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 861e8e589..55d45a648 100644
--- a/application/parser/file/docs_parser.py
+++ b/application/parser/file/docs_parser.py
@@ -7,7 +7,8 @@
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 @@ def _init_parser(self) -> Dict:
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 000000000..fd800d91c
--- /dev/null
+++ b/application/parser/file/image_parser.py
@@ -0,0 +1,27 @@
+"""Image parser.
+
+Contains parser for .png, .jpg, .jpeg files.
+
+"""
+from pathlib import Path
+import requests
+from typing import Dict, Union
+
+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/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json
index de4c228d6..6d736c6f0 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/package.json b/extensions/react-widget/package.json
index a1097403a..12a669397 100644
--- a/extensions/react-widget/package.json
+++ b/extensions/react-widget/package.json
@@ -32,7 +32,8 @@
"scripts": {
"build": "parcel build src/main.tsx --public-url ./",
"build:react": "parcel build src/index.ts",
- "dev": "parcel src/index.html -p 3000",
+ "serve": "parcel serve -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 ec9de47be..4bb24bae1 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/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx
index 1baa4c626..d6273eaa7 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, css } 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"
@@ -49,38 +49,8 @@ const sizesConfig = {
maxHeight: custom.maxHeight || '70vh',
}),
};
-
-const Overlay = styled.div`
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- z-index: 999;
- transition: opacity 0.5s;
-`
-const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
- all: initial;
- position: fixed;
- 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;
- }
- &.close {
- animation: closeBox 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
- }
- ${props => props.modal &&
- "transform : translate(50%,50%);"
- }
- align-items: center;
- text-align: left;
- @keyframes createBox {
- 0% {
+const createBox = keyframes`
+ 0% {
transform: scale(0.6);
}
90% {
@@ -89,10 +59,9 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
100% {
transform: scale(1);
}
- }
-
- @keyframes closeBox {
- 0% {
+`
+const closeBox = keyframes`
+ 0% {
transform: scale(1);
}
10% {
@@ -101,32 +70,9 @@ const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
100% {
transform: scale(0.6);
}
- }
-`;
-const StyledContainer = styled.div<{ isOpen: boolean }>`
- all: initial;
- max-height: ${(props) => props.theme.dimensions.maxHeight};
- max-width: ${(props) => props.theme.dimensions.maxWidth};
- position: relative;
- flex-direction: column;
- justify-content: space-between;
- bottom: 0;
- left: 0;
- background-color: ${(props) => props.theme.primary.bg};
- font-family: sans-serif;
- display: flex;
- border-radius: 12px;
- 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
- ? 'openContainer 150ms ease-in forwards'
- : 'closeContainer 250ms ease-in forwards'};
- @keyframes openContainer {
+`
+
+const openContainer = keyframes`
0% {
width: 200px;
height: 100px;
@@ -135,10 +81,9 @@ const StyledContainer = styled.div<{ isOpen: boolean }>`
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
border-radius: 12px;
- }
- }
- @keyframes closeContainer {
- 0% {
+ }`
+const closeContainer = keyframes`
+ 0% {
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
border-radius: 12px;
@@ -147,9 +92,9 @@ const StyledContainer = styled.div<{ isOpen: boolean }>`
width: 200px;
height: 100px;
}
- }
- @keyframes fadeIn {
- from {
+`
+const fadeIn = keyframes`
+ from {
opacity: 0;
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
@@ -161,9 +106,10 @@ const StyledContainer = styled.div<{ isOpen: boolean }>`
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
}
- }
- @keyframes fadeOut {
- from {
+`
+
+const fadeOut = keyframes`
+ from {
opacity: 1;
width: ${(props) => props.theme.dimensions.width};
height: ${(props) => props.theme.dimensions.height};
@@ -174,13 +120,80 @@ const StyledContainer = styled.div<{ isOpen: boolean }>`
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;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 999;
+ transition: opacity 0.5s;
+`
+
+
+const WidgetContainer = styled.div<{ modal?: boolean, isOpen?: boolean }>`
+ all: initial;
+ position: fixed;
+ right: ${props => props.modal ? '50%' : '10px'};
+ bottom: ${props => props.modal ? '50%' : '10px'};
+ z-index: 1001;
+ transform-origin:100% 100%;
+ display: block;
+ &.modal{
+ transform : translate(50%,50%);
}
+ &.open {
+ animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
+ }
+ &.close {
+ animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
+ }
+ align-items: center;
+ text-align: left;
+`;
+
+const StyledContainer = styled.div<{ isOpen: boolean }>`
+ all: initial;
+ 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;
+ bottom: 0;
+ left: 0;
+ background-color: ${(props) => props.theme.primary.bg};
+ font-family: sans-serif;
+ display: flex;
+ border-radius: 12px;
+ 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
+ ? 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`};
@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"};
@@ -198,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;
@@ -206,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;
@@ -478,7 +482,47 @@ 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);
+
+ React.useEffect(() => {
+ if (isFloatingButtonVisible)
+ setTimeout(() => setIsAnimatingButton(true), 250);
+ return () => {
+ setIsAnimatingButton(false)
+ }
+ }, [isFloatingButtonVisible])
+
+ const handleClose = () => {
+ setIsFloatingButtonVisible(true);
+ setOpen(false);
+ };
+ const handleOpen = () => {
+ setOpen(true);
+ setIsFloatingButtonVisible(false);
+ }
+ return (
+ <>
+
+
+ {buttonText}
+
+
+ >
+ )
+}
+export const WidgetCore = ({
apiHost = 'https://gptcloud.arc53.com',
apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a',
avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
@@ -488,25 +532,37 @@ 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('');
+ isOpen = false,
+ prefilledQuery = "",
+ handleClose
+}: WidgetCoreProps) => {
+ const [prompt, setPrompt] = React.useState("");
+ const [mounted, setMounted] = React.useState(false);
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();
+ React.useEffect(() => {
+ if (isOpen) {
+ setMounted(true); // Mount the component
+ appendQuery(prefilledQuery)
+ } else {
+ // Wait for animations before unmounting
+ const timeout = setTimeout(() => {
+ setMounted(false)
+ }, 250);
+ return () => clearTimeout(timeout);
+ }
+ }, [isOpen]);
+
+
+
const handleUserInterrupt = () => {
(status === 'loading') && setEventInterrupt(true);
}
@@ -606,144 +662,138 @@ export const DocsGPTWidget = ({
}
// 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 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];
-
+ if (!mounted) return null;
return (
- {open && size === 'large' &&
+ {isOpen && size === 'large' &&
}
-
-
- {buttonText}
-
-
- {
-
-
-
-
-
-
-
- {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 ?
-
-
-
-
Network Error
- {query.error}
-
-
- :
-
- .
- .
- .
-
-
- }
-
- }
- )
- })
- :
- }
-
-
-
- setPrompt(event.target.value)}
- type='text' placeholder="Ask your question" />
-
-
-
-
-
- Powered by
- DocsGPT
-
-
- }
-
+ {(
+
+
+
+
+
+
+
+
+
+ {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 ?
+
+
+
+
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
new file mode 100644
index 000000000..aff036d07
--- /dev/null
+++ b/extensions/react-widget/src/components/SearchBar.tsx
@@ -0,0 +1,398 @@
+import React from 'react'
+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';
+import { getOS } from '../utils/helper'
+const themes = {
+ dark: {
+ bg: '#000',
+ 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<{ 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};
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ transition: background-color 128ms linear;
+ &: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`
+ position: relative;
+ display: inline-block;
+`
+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);
+ border-radius: 12px;
+ padding: 8px;
+ width: 576px;
+ min-width: 96%;
+ z-index: 100;
+ height: 25vh;
+ overflow-y: auto;
+ 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;
+ 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;
+ 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};
+`
+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;
+word-break: break-all;
+ 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: ${(props) => props.theme.text};
+ opacity: 0.8;
+ }
+
+
+ h3 {
+ font-size: 14px;
+ }
+
+ p {
+ margin: 0px;
+ line-height: 1.35rem;
+ font-size: 12px;
+ }
+
+ 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;
+ }
+ a{
+ color: #007ee6;
+ }
+`
+const Toolkit = styled.kbd`
+ position: absolute;
+ right: 4px;
+ 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;
+`
+const Loader = styled.div`
+ margin: 2rem auto;
+ border: 4px solid ${props => props.theme.secondary.text};
+ 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;
+`;
+const InfoButton = styled.button`
+ cursor: pointer;
+ 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 = "256px"
+}: SearchBarProps) => {
+ const [input, setInput] = React.useState("");
+ const [loading, setLoading] = React.useState(false);
+ const [isWidgetOpen, setIsWidgetOpen] = React.useState(false);
+ const inputRef = 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);
+ const abortControllerRef = React.useRef(null)
+ const browserOS = getOS();
+ function isTouchDevice() {
+ 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 (
+ ((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(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('keydown', handleFocusSearch);
+ return () => {
+ setIsResultVisible(true);
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [])
+ React.useEffect(() => {
+ if (!input) {
+ setResults([]);
+ return;
+ }
+ setLoading(true);
+ if (debounceTimeout.current) {
+ clearTimeout(debounceTimeout.current);
+ }
+
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
+
+ debounceTimeout.current = setTimeout(() => {
+ getSearchResults(input, apiKey, apiHost, abortController.signal)
+ .then((data) => setResults(data))
+ .catch((err) => console.log(err))
+ .finally(() => setLoading(false));
+ }, 500);
+
+ return () => {
+ abortController.abort();
+ clearTimeout(debounceTimeout.current ?? undefined);
+ };
+ }, [input])
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ openWidget();
+ }
+ };
+ const openWidget = () => {
+ setIsWidgetOpen(true);
+ setIsResultVisible(false)
+ }
+ const handleClose = () => {
+ setIsWidgetOpen(false);
+ }
+ const md = new MarkdownIt();
+ return (
+
+
+
+ setIsResultVisible(true)}
+ ref={inputRef}
+ onSubmit={() => setIsWidgetOpen(true)}
+ onKeyDown={(e) => handleKeyDown(e)}
+ placeholder={placeholder}
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ />
+ {
+ input.length > 0 && isResultVisible && (
+
+
+ {
+ isTouch ?
+ "Ask the AI" :
+ <>
+ Press ↵ Enter to ask the AI
+ >
+ }
+
+ {!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
+ )
+ :
+
+ }
+
+ )
+ }
+ {
+ isTouch ?
+
+ {
+ setIsWidgetOpen(true)
+ }}
+ title={"Tap to Ask the AI"}>
+ Tap
+
+ :
+
+ {getKeyboardInstruction()}
+
+ }
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/extensions/react-widget/src/index.html b/extensions/react-widget/src/index.html
index 0f0710d5d..40eaad152 100644
--- a/extensions/react-widget/src/index.html
+++ b/extensions/react-widget/src/index.html
@@ -9,11 +9,11 @@
-
-
+ -->