Add lucide-react package and remove unused icons

This commit is contained in:
n4ze3m 2024-02-25 18:44:47 +05:30
parent 06b32176a9
commit 43f3727369
37 changed files with 610 additions and 574 deletions

View File

@ -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",

View File

@ -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"}
</span>
{props.isBot &&
props.isSearchingInternet &&
props.currentMessageIndex === props.totalMessages - 1 ? (
<WebSearch />
) : null}
<div className="flex flex-grow flex-col">
<Markdown message={props.message} />
</div>

View File

@ -0,0 +1,25 @@
import { useWebSearch } from "~store/web"
export const WebSearch = () => {
const {} = useWebSearch()
return (
<div className="gradient-border mt-4 flex w-56 items-center gap-4 rounded-lg bg-neutral-100 p-1ccc text-slate-900 dark:bg-neutral-800 dark:text-slate-50">
<div className="rounded p-1">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
</div>
<div className="text-sm font-semibold">Searching Web</div>
</div>
)
}

View File

@ -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<HTMLDivElement>(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 && (

View File

@ -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">
<XMarkIcon className="h-5 w-5" />
<X className="h-5 w-5" />
</button>
</div>
</div>
{/* <div className="flex gap-3 justify-end">
<Tooltip title="New Chat">
<button
onClick={clearChat}
className="text-gray-500 dark:text-gray-100 mr-3">
<ArrowPathIcon className="h-5 w-5" />
</button>
</Tooltip>
</div> */}
<div>
<div className="flex">
<form
@ -131,6 +122,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({
@ -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")}
/>
<div className="flex mt-4 !justify-end gap-3">
<Tooltip title="Voice Message">
<button
type="button"
onClick={() => {
if (isListening) {
stop()
} else {
start({
lang: speechToTextLanguage,
continuous: true
})
}
}}
className={`flex items-center justify-center dark:text-gray-300`}>
{!isListening ? (
<MicIcon className="h-5 w-5" />
) : (
<div className="relative">
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
<MicIcon className="h-5 w-5" />
</div>
)}
</button>
</Tooltip>
<Tooltip title="Upload Image">
<button
type="button"
onClick={() => {
inputRef.current?.click()
}}
className={`flex items-center justify-center dark:text-gray-300 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
<PhotoIcon className="h-5 w-5" />
</button>
</Tooltip>
{!isSending ? (
<Dropdown.Button
htmlType="submit"
disabled={
isSending || form.values.message.trim().length === 0
}
className="!justify-end !w-auto"
icon={
<div className="mt-4 flex justify-between items-center">
<div className="flex">
<Tooltip title="Search Internet">
<div className="inline-flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5">
className="w-5 h-5 dark:text-gray-300">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
}
menu={{
items: [
{
key: 1,
label: (
<Checkbox
onChange={(e) =>
setSendWhenEnter(e.target.checked)
}>
Send when Enter pressed
</Checkbox>
)
<Switch
value={webSearch}
onChange={(e) => setWebSearch(e)}
checkedChildren="On"
unCheckedChildren="Off"
/>
</div>
</Tooltip>
</div>
<div className="flex !justify-end gap-3">
<Tooltip title="Voice Message">
<button
type="button"
onClick={() => {
if (isListening) {
stop()
} else {
start({
lang: speechToTextLanguage,
continuous: true
})
}
]
}}>
<div className="inline-flex gap-2">
{sendWhenEnter ? (
}}
className={`flex items-center justify-center dark:text-gray-300`}>
{!isListening ? (
<MicIcon className="h-5 w-5" />
) : (
<div className="relative">
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
<MicIcon className="h-5 w-5" />
</div>
)}
</button>
</Tooltip>
<Tooltip title="Upload Image">
<button
type="button"
onClick={() => {
inputRef.current?.click()
}}
className={`flex items-center justify-center dark:text-gray-300 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
<ImageIcon className="h-5 w-5" />
</button>
</Tooltip>
{!isSending ? (
<Dropdown.Button
htmlType="submit"
disabled={
isSending || form.values.message.trim().length === 0
}
className="!justify-end !w-auto"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-5 w-5"
viewBox="0 0 24 24">
<path d="M9 10L4 15 9 20"></path>
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
className="w-5 h-5">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
/>
</svg>
) : null}
Submit
</div>
</Dropdown.Button>
) : (
<Tooltip title="Stop Streaming">
<button
type="button"
onClick={stopStreamingRequest}
className="text-gray-800 dark:text-gray-300">
<StopCircleIcon className="h-6 w-6" />
</button>
</Tooltip>
)}
}
menu={{
items: [
{
key: 1,
label: (
<Checkbox
value={sendWhenEnter}
onChange={(e) =>
setSendWhenEnter(e.target.checked)
}>
Send when Enter pressed
</Checkbox>
)
}
]
}}>
<div className="inline-flex gap-2">
{sendWhenEnter ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-5 w-5"
viewBox="0 0 24 24">
<path d="M9 10L4 15 9 20"></path>
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
</svg>
) : null}
Submit
</div>
</Dropdown.Button>
) : (
<Tooltip title="Stop Streaming">
<button
type="button"
onClick={stopStreamingRequest}
className="text-gray-800 dark:text-gray-300">
<StopCircleIcon className="h-6 w-6" />
</button>
</Tooltip>
)}
</div>
</div>
</div>
</form>

View File

@ -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">
<XMarkIcon className="h-5 w-5" />
<X className="h-5 w-5" />
</button>
</div>
</div>
@ -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"
}`}>
<PhotoIcon className="h-5 w-5" />
<ImageIcon className="h-5 w-5" />
</button>
</Tooltip>
<Dropdown.Button
@ -238,6 +236,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
key: 1,
label: (
<Checkbox
value={sendWhenEnter}
onChange={(e) =>
setSendWhenEnter(e.target.checked)
}>

View File

@ -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;
}
}
.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%;
}
}

View File

@ -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<AbortController | null>(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,
}
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const ArrowPathIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M3 12a9 9 0 019-9 9.75 9.75 0 016.74 2.74L21 8"></path>
<path d="M21 3v5h-5M21 12a9 9 0 01-9 9 9.75 9.75 0 01-6.74-2.74L3 16"></path>
<path d="M8 16H3v5"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const BoxesIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M2.97 12.92A2 2 0 002 14.63v3.24a2 2 0 00.97 1.71l3 1.8a2 2 0 002.06 0L12 19v-5.5l-5-3-4.03 2.42zM7 16.5l-4.74-2.85M7 16.5l5-3M7 16.5v5.17M12 13.5V19l3.97 2.38a2 2 0 002.06 0l3-1.8a2 2 0 00.97-1.71v-3.24a2 2 0 00-.97-1.71L17 10.5l-5 3zM17 16.5l-5-3M17 16.5l4.74-2.85M17 16.5v5.17"></path>
<path d="M7.97 4.42A2 2 0 007 6.13v4.37l5 3 5-3V6.13a2 2 0 00-.97-1.71l-3-1.8a2 2 0 00-2.06 0l-3 1.8zM12 8L7.26 5.15M12 8l4.74-2.85M12 13.5V8"></path>
</svg>
)
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const BrainCircuit: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className={className}
viewBox="0 0 24 24">
<path d="M12 4.5a2.5 2.5 0 00-4.96-.46 2.5 2.5 0 00-1.98 3 2.5 2.5 0 00-1.32 4.24 3 3 0 00.34 5.58 2.5 2.5 0 002.96 3.08 2.5 2.5 0 004.91.05L12 20V4.5zM16 8V5c0-1.1.9-2 2-2M12 13h4"></path>
<path d="M12 18h6a2 2 0 012 2v1M12 8h8M20.5 8a.5.5 0 11-1 0 .5.5 0 011 0zM16.5 13a.5.5 0 11-1 0 .5.5 0 011 0z"></path>
<path d="M20.5 21a.5.5 0 11-1 0 .5.5 0 011 0zM18.5 3a.5.5 0 11-1 0 .5.5 0 011 0z"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const CheckIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M20 6L9 17l-5-5"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const ChevronLeft: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M15 18l-6-6 6-6"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const ClipboardIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
<path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const CogIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
)
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const Download: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<path d="M7 10L12 15 17 10"></path>
<path d="M12 15L12 3"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const EllipsisHorizontalIcon = ({ className }: Props) => {
return <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}
>
<circle cx="12" cy="12" r="1"></circle>
<circle cx="12" cy="5" r="1"></circle>
<circle cx="12" cy="19" r="1"></circle>
</svg>
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const GithubIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className={className}
viewBox="0 0 24 24">
<path d="M15 22v-4a4.8 4.8 0 00-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 004 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4M9 18c-4.51 2-5-2-7-2"></path>
</svg>
)
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const MicIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M12 2a3 3 0 00-3 3v7a3 3 0 006 0V5a3 3 0 00-3-3z"></path>
<path d="M19 10v2a7 7 0 01-14 0v-2"></path>
<path d="M12 19L12 22"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const Moon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M12 3a6 6 0 009 9 9 9 0 11-9-9z"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const PanelLeftIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className={className}
viewBox="0 0 24 24">
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
<path d="M9 3v18"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const PencilIcon = ({ className }: Props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M17 3a2.85 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5zM15 5l4 4"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const PencilSquareIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M12 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path>
<path d="M18.375 2.625a2.121 2.121 0 113 3L12 15l-4 1 1-4z"></path>
</svg>
)
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const PhotoIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
<circle cx="9" cy="9" r="2"></circle>
<path d="M21 15l-3.086-3.086a2 2 0 00-2.828 0L6 21"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const RotateCcw: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M3 12a9 9 0 109-9 9.75 9.75 0 00-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
</svg>
)
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const SquarePen: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className={className}
viewBox="0 0 24 24">
<path d="M12 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path>
<path d="M18.375 2.625a2.121 2.121 0 113 3L12 15l-4 1 1-4z"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const StopCircleIcon: React.FC<Props> = ({ className }) => {
return <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M9 9H15V15H9z"></path>
</svg>
}

View File

@ -1,20 +0,0 @@
type Props = {
className: string
}
export const Sun: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path>
</svg>
)
}

View File

@ -1,21 +0,0 @@
type Props = {
className: string
}
export const Trash: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className={className}>
<path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<path d="M10 11L10 17"></path>
<path d="M14 11L14 17"></path>
</svg>
)
}

View File

@ -1,19 +0,0 @@
type Props = {
className: string
}
export const XMarkIcon: React.FC<Props> = ({ className }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className={className}
viewBox="0 0 24 24">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
)
}

View File

@ -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<Document<Record<string, any>>[]> {
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 })]
}
}

View File

@ -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)
}

View File

@ -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<State>((set) => ({
@ -72,5 +76,9 @@ export const useStoreMessageOption = create<State>((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 }),
}))

15
src/store/web.tsx Normal file
View File

@ -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<State>((set) => ({
state: "searching",
text: "Searching Google",
setText: (text) => set({ text }),
setState: (state) => set({ state })
}))

View File

@ -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<Record<string, any>>[] = [];
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
}

20
src/web/web.ts Normal file
View File

@ -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 source="${result.url}" id="${idx}">${result.content}</result>`).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 ''
}
}

View File

@ -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"