From 43f3727369ad0455abacd71eb1f7e9b78bfb989d Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 25 Feb 2024 18:44:47 +0530 Subject: [PATCH] Add lucide-react package and remove unused icons --- package.json | 1 + src/components/Common/Playground/Message.tsx | 16 +- .../Common/Playground/WebSearch.tsx | 25 ++ .../Option/Playground/PlaygroundChat.tsx | 3 +- .../Option/Playground/PlaygroundForm.tsx | 243 ++++++++++-------- src/components/Sidepanel/Chat/form.tsx | 11 +- src/css/tailwind.css | 50 +++- src/hooks/useMessageOption.tsx | 240 ++++++++++++++++- src/icons/ArrowPathIcon.tsx | 21 -- src/icons/BoxesIcon.tsx | 20 -- src/icons/BrainCircuit.tsx | 21 -- src/icons/CheckIcon.tsx | 19 -- src/icons/ChevronLeft.tsx | 19 -- src/icons/ClipboardIcon.tsx | 20 -- src/icons/CogIcon.tsx | 20 -- src/icons/Download.tsx | 21 -- src/icons/EllipsisHorizontalIcon.tsx | 20 -- src/icons/GithubIcon.tsx | 19 -- src/icons/MicIcon.tsx | 21 -- src/icons/Moon.tsx | 19 -- src/icons/PanelLeftIcon.tsx | 20 -- src/icons/PencilIcon.tsx | 19 -- src/icons/PencilSquareIcon.tsx | 20 -- src/icons/PhotoIcon.tsx | 21 -- src/icons/RotateCcw.tsx | 20 -- src/icons/SquarePen.tsx | 20 -- src/icons/StopCircleIcon.tsx | 19 -- src/icons/Sun.tsx | 20 -- src/icons/Trash.tsx | 21 -- src/icons/XMarkIcon.tsx | 19 -- src/loader/html.ts | 25 +- src/services/ollama.ts | 20 ++ src/store/option.tsx | 10 +- src/store/web.tsx | 15 ++ src/web/local-google.ts | 61 +++++ src/web/web.ts | 20 ++ yarn.lock | 5 + 37 files changed, 610 insertions(+), 574 deletions(-) create mode 100644 src/components/Common/Playground/WebSearch.tsx delete mode 100644 src/icons/ArrowPathIcon.tsx delete mode 100644 src/icons/BoxesIcon.tsx delete mode 100644 src/icons/BrainCircuit.tsx delete mode 100644 src/icons/CheckIcon.tsx delete mode 100644 src/icons/ChevronLeft.tsx delete mode 100644 src/icons/ClipboardIcon.tsx delete mode 100644 src/icons/CogIcon.tsx delete mode 100644 src/icons/Download.tsx delete mode 100644 src/icons/EllipsisHorizontalIcon.tsx delete mode 100644 src/icons/GithubIcon.tsx delete mode 100644 src/icons/MicIcon.tsx delete mode 100644 src/icons/Moon.tsx delete mode 100644 src/icons/PanelLeftIcon.tsx delete mode 100644 src/icons/PencilIcon.tsx delete mode 100644 src/icons/PencilSquareIcon.tsx delete mode 100644 src/icons/PhotoIcon.tsx delete mode 100644 src/icons/RotateCcw.tsx delete mode 100644 src/icons/SquarePen.tsx delete mode 100644 src/icons/StopCircleIcon.tsx delete mode 100644 src/icons/Sun.tsx delete mode 100644 src/icons/Trash.tsx delete mode 100644 src/icons/XMarkIcon.tsx create mode 100644 src/store/web.tsx create mode 100644 src/web/web.ts diff --git a/package.json b/package.json index 2944cb5..714c54d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dayjs": "^1.11.10", "html-to-text": "^9.0.5", "langchain": "^0.1.9", + "lucide-react": "^0.340.0", "plasmo": "0.84.1", "property-information": "^6.4.1", "react": "18.2.0", diff --git a/src/components/Common/Playground/Message.tsx b/src/components/Common/Playground/Message.tsx index 50e4902..5bb3404 100644 --- a/src/components/Common/Playground/Message.tsx +++ b/src/components/Common/Playground/Message.tsx @@ -1,9 +1,8 @@ import Markdown from "../../Common/Markdown" import React from "react" import { Image, Tooltip } from "antd" -import { ClipboardIcon } from "~icons/ClipboardIcon" -import { CheckIcon } from "~icons/CheckIcon" -import { ArrowPathIcon } from "~icons/ArrowPathIcon" +import { WebSearch } from "./WebSearch" +import { CheckIcon, ClipboardIcon } from "lucide-react" type Props = { message: string @@ -17,9 +16,8 @@ type Props = { totalMessages: number onRengerate: () => void isProcessing: boolean - webSearch?: { - - } + webSearch?: {} + isSearchingInternet?: boolean } export const PlaygroundMessage = (props: Props) => { @@ -49,6 +47,12 @@ export const PlaygroundMessage = (props: Props) => { {props.isBot ? props.name : "You"} + {props.isBot && + props.isSearchingInternet && + props.currentMessageIndex === props.totalMessages - 1 ? ( + + ) : null} +
diff --git a/src/components/Common/Playground/WebSearch.tsx b/src/components/Common/Playground/WebSearch.tsx new file mode 100644 index 0000000..cd58cc0 --- /dev/null +++ b/src/components/Common/Playground/WebSearch.tsx @@ -0,0 +1,25 @@ +import { useWebSearch } from "~store/web" + +export const WebSearch = () => { + const {} = useWebSearch() + return ( +
+
+ + + +
+
Searching Web
+
+ ) +} diff --git a/src/components/Option/Playground/PlaygroundChat.tsx b/src/components/Option/Playground/PlaygroundChat.tsx index 2d73fce..af56241 100644 --- a/src/components/Option/Playground/PlaygroundChat.tsx +++ b/src/components/Option/Playground/PlaygroundChat.tsx @@ -4,7 +4,7 @@ import { PlaygroundEmpty } from "./PlaygroundEmpty" import { PlaygroundMessage } from "~components/Common/Playground/Message" export const PlaygroundChat = () => { - const { messages, streaming, regenerateLastMessage } = useMessageOption() + const { messages, streaming, regenerateLastMessage, isSearchingInternet } = useMessageOption() const divRef = React.useRef(null) React.useEffect(() => { if (divRef.current) { @@ -30,6 +30,7 @@ export const PlaygroundChat = () => { totalMessages={messages.length} onRengerate={regenerateLastMessage} isProcessing={streaming} + isSearchingInternet={isSearchingInternet} /> ))} {messages.length > 0 && ( diff --git a/src/components/Option/Playground/PlaygroundForm.tsx b/src/components/Option/Playground/PlaygroundForm.tsx index 9ae6558..dbe1e21 100644 --- a/src/components/Option/Playground/PlaygroundForm.tsx +++ b/src/components/Option/Playground/PlaygroundForm.tsx @@ -4,14 +4,12 @@ import React from "react" import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" import { toBase64 } from "~libs/to-base64" import { useMessageOption } from "~hooks/useMessageOption" -import { Checkbox, Dropdown, Tooltip } from "antd" +import { Checkbox, Dropdown, Switch, Tooltip } from "antd" import { Image } from "antd" import { useSpeechRecognition } from "~hooks/useSpeechRecognition" -import { MicIcon } from "~icons/MicIcon" -import { StopCircleIcon } from "~icons/StopCircleIcon" -import { PhotoIcon } from "~icons/PhotoIcon" -import { XMarkIcon } from "~icons/XMarkIcon" import { useWebUI } from "~store/webui" +import { defaultEmbeddingModelForRag } from "~services/ollama" +import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react" type Props = { dropedFile: File | undefined @@ -68,7 +66,9 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { chatMode, speechToTextLanguage, stopStreamingRequest, - streaming: isSending + streaming: isSending, + webSearch, + setWebSearch } = useMessageOption() const { isListening, start, stop, transcript } = useSpeechRecognition() @@ -110,19 +110,10 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { form.setFieldValue("image", "") }} className="flex items-center justify-center absolute top-0 m-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100"> - + - {/*
- - - -
*/}
{ form.setFieldError("message", "Please select a model") return } + if (webSearch) { + const defaultEM = await defaultEmbeddingModelForRag() + if (!defaultEM) { + form.setFieldError( + "message", + "Please set an embedding model on the Settings > Ollama page" + ) + return + } + } form.reset() resetHeight() await sendMessage({ @@ -167,6 +168,16 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { form.setFieldError("message", "Please select a model") return } + if (webSearch) { + const defaultEM = await defaultEmbeddingModelForRag() + if (!defaultEM) { + form.setFieldError( + "message", + "Please set an embedding model on the Settings > Ollama page" + ) + return + } + } form.reset() resetHeight() await sendMessage({ @@ -177,7 +188,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { } }} ref={textareaRef} - className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none sm:text-sm focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100" + className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100" required rows={1} style={{ minHeight: "60px" }} @@ -185,108 +196,136 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { placeholder="Type a message..." {...form.getInputProps("message")} /> -
- - - - - - - {!isSending ? ( - +
+ +
+ className="w-5 h-5 dark:text-gray-300"> - } - menu={{ - items: [ - { - key: 1, - label: ( - - setSendWhenEnter(e.target.checked) - }> - Send when Enter pressed - - ) + setWebSearch(e)} + checkedChildren="On" + unCheckedChildren="Off" + /> +
+
+
+
+ + + + + + + {!isSending ? ( + - - + className="w-5 h-5"> + - ) : null} - Submit -
-
- ) : ( - - - - )} + } + menu={{ + items: [ + { + key: 1, + label: ( + + setSendWhenEnter(e.target.checked) + }> + Send when Enter pressed + + ) + } + ] + }}> +
+ {sendWhenEnter ? ( + + + + + ) : null} + Submit +
+ + ) : ( + + + + )} +
diff --git a/src/components/Sidepanel/Chat/form.tsx b/src/components/Sidepanel/Chat/form.tsx index 778a7a4..f8d287d 100644 --- a/src/components/Sidepanel/Chat/form.tsx +++ b/src/components/Sidepanel/Chat/form.tsx @@ -6,11 +6,9 @@ import { useMessage } from "~hooks/useMessage" import { toBase64 } from "~libs/to-base64" import { Checkbox, Dropdown, Image, Tooltip } from "antd" import { useSpeechRecognition } from "~hooks/useSpeechRecognition" -import { MicIcon } from "~icons/MicIcon" -import { PhotoIcon } from "~icons/PhotoIcon" -import { XMarkIcon } from "~icons/XMarkIcon" import { useWebUI } from "~store/webui" import { defaultEmbeddingModelForRag } from "~services/ollama" +import { ImageIcon, MicIcon, X } from "lucide-react" type Props = { dropedFile: File | undefined @@ -88,7 +86,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => { form.setFieldValue("image", "") }} className="flex items-center justify-center absolute top-0 m-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100"> - + @@ -166,7 +164,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => { } }} ref={textareaRef} - className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none sm:text-sm focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100" + className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100" required rows={1} style={{ minHeight: "60px" }} @@ -208,7 +206,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => { className={`flex items-center justify-center dark:text-gray-300 ${ chatMode === "rag" ? "hidden" : "block" }`}> - + { key: 1, label: ( setSendWhenEnter(e.target.checked) }> diff --git a/src/css/tailwind.css b/src/css/tailwind.css index bc57db0..f55882d 100644 --- a/src/css/tailwind.css +++ b/src/css/tailwind.css @@ -1,12 +1,11 @@ @font-face { - font-family: 'font'; - src: url('font.ttf') format('truetype'); + font-family: "font"; + src: url("font.ttf") format("truetype"); } * { - font-family: 'font' !important; + font-family: "font" !important; } - @tailwind base; @tailwind components; @tailwind utilities; @@ -14,4 +13,45 @@ .ant-select-selection-search-input { border: none !important; box-shadow: none !important; -} \ No newline at end of file +} + +.gradient-border { + --borderWidth: 3px; + position: relative; + border-radius: var(--borderWidth); +} +.gradient-border:after { + content: ""; + position: absolute; + top: calc(-1 * var(--borderWidth)); + left: calc(-1 * var(--borderWidth)); + height: calc(100% + var(--borderWidth) * 2); + width: calc(100% + var(--borderWidth) * 2); + background: linear-gradient( + 60deg, + #f79533, + #f37055, + #ef4e7b, + #a166ab, + #5073b8, + #1098ad, + #07b39b, + #6fba82 + ); + border-radius: calc(2 * var(--borderWidth)); + z-index: -1; + animation: animatedgradient 3s ease alternate infinite; + background-size: 300% 300%; +} + +@keyframes animatedgradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} diff --git a/src/hooks/useMessageOption.tsx b/src/hooks/useMessageOption.tsx index 18ea58f..037f869 100644 --- a/src/hooks/useMessageOption.tsx +++ b/src/hooks/useMessageOption.tsx @@ -13,6 +13,7 @@ import { useStoreMessageOption } from "~store/option" import { removeMessageUsingHistoryId, saveHistory, saveMessage } from "~libs/db" import { useNavigate } from "react-router-dom" import { notification } from "antd" +import { getSystemPromptForWeb } from "~web/web" export type BotResponse = { bot: { @@ -93,15 +94,19 @@ export const useMessageOption = () => { chatMode, setChatMode, speechToTextLanguage, - setSpeechToTextLanguage + setSpeechToTextLanguage, + webSearch, + setWebSearch, + isSearchingInternet, + setIsSearchingInternet } = useStoreMessageOption() + const navigate = useNavigate() const abortControllerRef = React.useRef(null) const clearChat = () => { - // stopStreamingRequest() setMessages([]) setHistory([]) setHistoryId(null) @@ -112,6 +117,224 @@ export const useMessageOption = () => { navigate("/") } + const searchChatMode = async ( + message: string, + image: string, + isRegenerate: boolean + ) => { + const url = await getOllamaURL() + + if (image.length > 0) { + image = `data:image/jpeg;base64,${image.split(",")[1]}` + } + abortControllerRef.current = new AbortController() + + const ollama = new ChatOllama({ + model: selectedModel, + baseUrl: cleanUrl(url) + }) + + let newMessage: Message[] = [ + ...messages, + { + isBot: false, + name: "You", + message, + sources: [], + images: [image] + }, + { + isBot: true, + name: selectedModel, + message: "▋", + sources: [] + } + ] + + const appendingIndex = newMessage.length - 1 + if (!isRegenerate) { + setMessages(newMessage) + } + + try { + setIsSearchingInternet(true) + const prompt = await getSystemPromptForWeb(message) + setIsSearchingInternet(false) + + message = message.trim().replaceAll("\n", " ") + + let humanMessage = new HumanMessage({ + content: [ + { + text: message, + type: "text" + } + ] + }) + if (image.length > 0) { + humanMessage = new HumanMessage({ + content: [ + { + text: message, + type: "text" + }, + { + image_url: image, + type: "image_url" + } + ] + }) + } + + const applicationChatHistory = generateHistory(history) + + if (prompt) { + applicationChatHistory.unshift( + new SystemMessage({ + content: [ + { + text: prompt, + type: "text" + } + ] + }) + ) + } + + const chunks = await ollama.stream( + [...applicationChatHistory, humanMessage], + { + signal: abortControllerRef.current.signal + } + ) + let count = 0 + for await (const chunk of chunks) { + if (count === 0) { + setIsProcessing(true) + newMessage[appendingIndex].message = chunk.content + "▋" + setMessages(newMessage) + } else { + newMessage[appendingIndex].message = + newMessage[appendingIndex].message.slice(0, -1) + + chunk.content + + "▋" + setMessages(newMessage) + } + + count++ + } + + newMessage[appendingIndex].message = newMessage[ + appendingIndex + ].message.slice(0, -1) + + if (!isRegenerate) { + setHistory([ + ...history, + { + role: "user", + content: message, + image + }, + { + role: "assistant", + content: newMessage[appendingIndex].message + } + ]) + } else { + setHistory([ + ...history, + { + role: "assistant", + content: newMessage[appendingIndex].message + } + ]) + } + + if (historyId) { + if (!isRegenerate) { + await saveMessage(historyId, selectedModel, "user", message, [image]) + } + await saveMessage( + historyId, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + } else { + const newHistoryId = await saveHistory(message) + await saveMessage(newHistoryId.id, selectedModel, "user", message, [ + image + ]) + await saveMessage( + newHistoryId.id, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + setHistoryId(newHistoryId.id) + } + + setIsProcessing(false) + setStreaming(false) + } catch (e) { + console.log(e) + + if (e?.name === "AbortError") { + newMessage[appendingIndex].message = newMessage[ + appendingIndex + ].message.slice(0, -1) + + setHistory([ + ...history, + { + role: "user", + content: message, + image + }, + { + role: "assistant", + content: newMessage[appendingIndex].message + } + ]) + + if (historyId) { + await saveMessage(historyId, selectedModel, "user", message, [image]) + await saveMessage( + historyId, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + } else { + const newHistoryId = await saveHistory(message) + await saveMessage(newHistoryId.id, selectedModel, "user", message, [ + image + ]) + await saveMessage( + newHistoryId.id, + selectedModel, + "assistant", + newMessage[appendingIndex].message, + [] + ) + setHistoryId(newHistoryId.id) + } + } else { + notification.error({ + message: "Error", + description: e?.message || "Something went wrong" + }) + } + + setIsProcessing(false) + setStreaming(false) + } + } + const normalChatMode = async ( message: string, image: string, @@ -338,9 +561,11 @@ export const useMessageOption = () => { isRegenerate?: boolean }) => { setStreaming(true) - // const web = await localGoogleSearch(message) - // console.log(web) - await normalChatMode(message, image, isRegenerate) + if (webSearch) { + await searchChatMode(message, image, isRegenerate) + } else { + await normalChatMode(message, image, isRegenerate) + } } const regenerateLastMessage = async () => { @@ -387,6 +612,9 @@ export const useMessageOption = () => { setChatMode, speechToTextLanguage, setSpeechToTextLanguage, - regenerateLastMessage + regenerateLastMessage, + webSearch, + setWebSearch, + isSearchingInternet, } } diff --git a/src/icons/ArrowPathIcon.tsx b/src/icons/ArrowPathIcon.tsx deleted file mode 100644 index ec135ca..0000000 --- a/src/icons/ArrowPathIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const ArrowPathIcon: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/BoxesIcon.tsx b/src/icons/BoxesIcon.tsx deleted file mode 100644 index 71e75af..0000000 --- a/src/icons/BoxesIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const BoxesIcon: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/BrainCircuit.tsx b/src/icons/BrainCircuit.tsx deleted file mode 100644 index 985c7ab..0000000 --- a/src/icons/BrainCircuit.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const BrainCircuit: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/CheckIcon.tsx b/src/icons/CheckIcon.tsx deleted file mode 100644 index bb34e5a..0000000 --- a/src/icons/CheckIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const CheckIcon: React.FC = ({ className }) => { - return ( - - - - ) -} diff --git a/src/icons/ChevronLeft.tsx b/src/icons/ChevronLeft.tsx deleted file mode 100644 index d48146a..0000000 --- a/src/icons/ChevronLeft.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const ChevronLeft: React.FC = ({ className }) => { - return ( - - - - ) -} diff --git a/src/icons/ClipboardIcon.tsx b/src/icons/ClipboardIcon.tsx deleted file mode 100644 index 05a2b8d..0000000 --- a/src/icons/ClipboardIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const ClipboardIcon: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/CogIcon.tsx b/src/icons/CogIcon.tsx deleted file mode 100644 index 2a41794..0000000 --- a/src/icons/CogIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const CogIcon: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/Download.tsx b/src/icons/Download.tsx deleted file mode 100644 index fa055b3..0000000 --- a/src/icons/Download.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const Download: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/EllipsisHorizontalIcon.tsx b/src/icons/EllipsisHorizontalIcon.tsx deleted file mode 100644 index 7d44a59..0000000 --- a/src/icons/EllipsisHorizontalIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const EllipsisHorizontalIcon = ({ className }: Props) => { - return - - - - -} \ No newline at end of file diff --git a/src/icons/GithubIcon.tsx b/src/icons/GithubIcon.tsx deleted file mode 100644 index d2286f3..0000000 --- a/src/icons/GithubIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const GithubIcon: React.FC = ({ className }) => { - return ( - - - - ) -} diff --git a/src/icons/MicIcon.tsx b/src/icons/MicIcon.tsx deleted file mode 100644 index abc39ce..0000000 --- a/src/icons/MicIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const MicIcon: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/Moon.tsx b/src/icons/Moon.tsx deleted file mode 100644 index 514b8ff..0000000 --- a/src/icons/Moon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const Moon: React.FC = ({ className }) => { - return ( - - - - ) -} diff --git a/src/icons/PanelLeftIcon.tsx b/src/icons/PanelLeftIcon.tsx deleted file mode 100644 index e1800f0..0000000 --- a/src/icons/PanelLeftIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const PanelLeftIcon: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/PencilIcon.tsx b/src/icons/PencilIcon.tsx deleted file mode 100644 index 9f9df61..0000000 --- a/src/icons/PencilIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const PencilIcon = ({ className }: Props) => { - return ( - - - - ) -} diff --git a/src/icons/PencilSquareIcon.tsx b/src/icons/PencilSquareIcon.tsx deleted file mode 100644 index 0bd7ff6..0000000 --- a/src/icons/PencilSquareIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const PencilSquareIcon: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/PhotoIcon.tsx b/src/icons/PhotoIcon.tsx deleted file mode 100644 index 5309899..0000000 --- a/src/icons/PhotoIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const PhotoIcon: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/RotateCcw.tsx b/src/icons/RotateCcw.tsx deleted file mode 100644 index 05cd1af..0000000 --- a/src/icons/RotateCcw.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const RotateCcw: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/SquarePen.tsx b/src/icons/SquarePen.tsx deleted file mode 100644 index b0a147c..0000000 --- a/src/icons/SquarePen.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const SquarePen: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/StopCircleIcon.tsx b/src/icons/StopCircleIcon.tsx deleted file mode 100644 index bc434eb..0000000 --- a/src/icons/StopCircleIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const StopCircleIcon: React.FC = ({ className }) => { - return - - - -} \ No newline at end of file diff --git a/src/icons/Sun.tsx b/src/icons/Sun.tsx deleted file mode 100644 index 5960176..0000000 --- a/src/icons/Sun.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type Props = { - className: string -} - -export const Sun: React.FC = ({ className }) => { - return ( - - - - - ) -} diff --git a/src/icons/Trash.tsx b/src/icons/Trash.tsx deleted file mode 100644 index 81340f2..0000000 --- a/src/icons/Trash.tsx +++ /dev/null @@ -1,21 +0,0 @@ -type Props = { - className: string -} - -export const Trash: React.FC = ({ className }) => { - return ( - - - - - - ) -} diff --git a/src/icons/XMarkIcon.tsx b/src/icons/XMarkIcon.tsx deleted file mode 100644 index 0b5bb52..0000000 --- a/src/icons/XMarkIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type Props = { - className: string -} - -export const XMarkIcon: React.FC = ({ className }) => { - return ( - - - - ) -} diff --git a/src/loader/html.ts b/src/loader/html.ts index eaaeb67..f379c67 100644 --- a/src/loader/html.ts +++ b/src/loader/html.ts @@ -3,12 +3,6 @@ import { Document } from "@langchain/core/documents" import { compile } from "html-to-text" import { chromeRunTime } from "~libs/runtime" -const isPDFFetch = async (url: string) => { - await chromeRunTime(url) - const response = await fetch(url) - const blob = await response.blob() - return blob.type === "application/pdf" -} export interface WebLoaderParams { html: string url: string @@ -16,8 +10,7 @@ export interface WebLoaderParams { export class PageAssistHtmlLoader extends BaseDocumentLoader - implements WebLoaderParams -{ + implements WebLoaderParams { html: string url: string @@ -35,4 +28,20 @@ export class PageAssistHtmlLoader const metadata = { source: this.url } return [new Document({ pageContent: text, metadata })] } + + async loadByURL(): Promise>[]> { + await chromeRunTime(this.url) + const fetchHTML = await fetch(this.url) + const html = await fetchHTML.text() + const htmlCompiler = compile({ + wordwrap: false, + selectors: [ + { selector: "img", format: "skip" }, + { selector: "script", format: "skip" } + ] + }) + const text = htmlCompiler(html) + const metadata = { source: this.url } + return [new Document({ pageContent: text, metadata })] + } } diff --git a/src/services/ollama.ts b/src/services/ollama.ts index f3c0f92..4db0f53 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -12,6 +12,13 @@ const DEFAULT_RAG_QUESTION_PROMPT = const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer in markdown:` + +const DEFAULT_WEBSEARCH_PROMP = `You are a helpful assistant that can answer any questions. You can use the following search results in case you want to answer questions about anything in real-time. The current date and time are {current_date_time}. + +Search results: + +{search_results}` + export const getOllamaURL = async () => { const ollamaURL = await storage.get("ollamaURL") if (!ollamaURL || ollamaURL.length === 0) { @@ -247,3 +254,16 @@ export const saveForRag = async ( await setDefaultEmbeddingChunkSize(chunkSize) await setDefaultEmbeddingChunkOverlap(overlap) } + + +export const getWebSearchPrompt = async () => { + const prompt = await storage.get("webSearchPrompt") + if (!prompt || prompt.length === 0) { + return DEFAULT_WEBSEARCH_PROMP + } + return prompt +} + +export const setWebSearchPrompt = async (prompt: string) => { + await storage.set("webSearchPrompt", prompt) +} \ No newline at end of file diff --git a/src/store/option.tsx b/src/store/option.tsx index 759f84c..802f40e 100644 --- a/src/store/option.tsx +++ b/src/store/option.tsx @@ -47,6 +47,10 @@ type State = { setIsEmbedding: (isEmbedding: boolean) => void speechToTextLanguage: string setSpeechToTextLanguage: (language: string) => void + webSearch: boolean; + setWebSearch: (webSearch: boolean) => void; + isSearchingInternet: boolean; + setIsSearchingInternet: (isSearchingInternet: boolean) => void; } export const useStoreMessageOption = create((set) => ({ @@ -72,5 +76,9 @@ export const useStoreMessageOption = create((set) => ({ chatMode: "normal", setChatMode: (chatMode) => set({ chatMode }), isEmbedding: false, - setIsEmbedding: (isEmbedding) => set({ isEmbedding }) + setIsEmbedding: (isEmbedding) => set({ isEmbedding }), + webSearch: false, + setWebSearch: (webSearch) => set({ webSearch }), + isSearchingInternet: false, + setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }), })) diff --git a/src/store/web.tsx b/src/store/web.tsx new file mode 100644 index 0000000..9474700 --- /dev/null +++ b/src/store/web.tsx @@ -0,0 +1,15 @@ +import { create } from "zustand" + +type State = { + state: "searching" | "clicked" | "embeddings" | "done" + text: string + setText: (text: string) => void + setState: (state: "searching" | "clicked" | "embeddings" | "done") => void +} + +export const useWebSearch = create((set) => ({ + state: "searching", + text: "Searching Google", + setText: (text) => set({ text }), + setState: (state) => set({ state }) +})) diff --git a/src/web/local-google.ts b/src/web/local-google.ts index 3c3f235..cb2193a 100644 --- a/src/web/local-google.ts +++ b/src/web/local-google.ts @@ -1,12 +1,21 @@ +import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" +import type { Document } from "@langchain/core/documents" +import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" +import { MemoryVectorStore } from "langchain/vectorstores/memory" import { cleanUrl } from "~libs/clean-url" import { chromeRunTime } from "~libs/runtime" +import { PageAssistHtmlLoader } from "~loader/html" +import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getOllamaURL } from "~services/ollama" const BLOCKED_HOSTS = [ "google.com", "youtube.com", "twitter.com", + "linkedin.com", ] +const TOTAL_SEARCH_RESULTS = 2 + export const localGoogleSearch = async (query: string) => { await chromeRunTime( cleanUrl("https://www.google.com/search?hl=en&q=" + query) @@ -43,3 +52,55 @@ export const localGoogleSearch = async (query: string) => { .filter((result) => result.title && result.link) return filteredSearchResults } + + +export const webSearch = async (query: string) => { + const results = await localGoogleSearch(query) + const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) + + const docs: Document>[] = []; + for (const result of searchResults) { + const loader = new PageAssistHtmlLoader({ + html: "", + url: result.link + }) + + const documents = await loader.loadByURL() + + documents.forEach((doc) => { + docs.push(doc) + }) + } + const ollamaUrl = await getOllamaURL() + + const embeddingModle = await defaultEmbeddingModelForRag() + const ollamaEmbedding = new OllamaEmbeddings({ + model: embeddingModle || "", + baseUrl: cleanUrl(ollamaUrl), + }) + + const chunkSize = await defaultEmbeddingChunkSize(); + const chunkOverlap = await defaultEmbeddingChunkOverlap(); + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize, + chunkOverlap, + }) + + const chunks = await textSplitter.splitDocuments(docs) + + const store = new MemoryVectorStore(ollamaEmbedding) + + await store.addDocuments(chunks) + + + const resultsWithEmbeddings = await store.similaritySearch(query, 3) + + const searchResult = resultsWithEmbeddings.map((result) => { + return { + url: result.metadata.url, + content: result.pageContent + } + }) + + return searchResult +} \ No newline at end of file diff --git a/src/web/web.ts b/src/web/web.ts new file mode 100644 index 0000000..1ed5f49 --- /dev/null +++ b/src/web/web.ts @@ -0,0 +1,20 @@ +import { getWebSearchPrompt } from "~services/ollama" +import { webSearch } from "./local-google" + +export const getSystemPromptForWeb = async (query: string) => { + try { + const search = await webSearch(query) + + const search_results = search.map((result, idx) => `${result.content}`).join("\n") + + const current_date_time = new Date().toLocaleString() + + const system = await getWebSearchPrompt(); + + const prompt = system.replace("{current_date_time}", current_date_time).replace("{search_results}", search_results) + + return prompt + } catch (e) { + return '' + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3d33606..1dc6031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4967,6 +4967,11 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +lucide-react@^0.340.0: + version "0.340.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.340.0.tgz#67a6fac6a5e257f2036dffae0dd94d6ccb28ce8e" + integrity sha512-mWzYhbyy2d+qKuKHh+GWElPwa+kIquTnKbmSLGWOuZy+bjfZCkYD8DQWVFlqI4mQwc4HNxcqcOvtQ7ZS2PwURg== + magic-string@^0.30.0: version "0.30.6" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c"