Merge pull request #12 from n4ze3m/next

Next
This commit is contained in:
Muhammed Nazeem 2024-03-04 20:19:38 +05:30 committed by GitHub
commit 77c1fd6e3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1252 additions and 472 deletions

View File

@ -1,7 +1,7 @@
{
"name": "pageassist",
"displayName": "Page Assist - A Web UI for Local AI Models",
"version": "1.0.4",
"version": "1.0.5",
"description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m",
"scripts": {
@ -21,7 +21,6 @@
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.19",
"@types/pdf-parse": "^1.1.4",
"antd": "^5.13.3",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
@ -39,7 +38,6 @@
"rehype-mathjax": "4.0.3",
"remark-gfm": "3.0.1",
"remark-math": "5.1.1",
"voy-search": "^0.6.3",
"zustand": "^4.5.0"
},
"devDependencies": {

View File

@ -96,7 +96,6 @@ chrome.runtime.onMessage.addListener(async (message) => {
clearBadge()
}, 5000)
}
console.log("Pulling model", message.modelName)
await streamDownload(ollamaURL, message.modelName)
}

View File

@ -0,0 +1,205 @@
import React, { useState } from "react"
import { useLocation, NavLink } from "react-router-dom"
import { Sidebar } from "../Option/Sidebar"
import { Drawer, Select, Tooltip } from "antd"
import { useQuery } from "@tanstack/react-query"
import { getAllModels } from "~services/ollama"
import { useMessageOption } from "~hooks/useMessageOption"
import {
ChevronLeft,
CogIcon,
ComputerIcon,
GithubIcon,
PanelLeftIcon,
SquarePen,
ZapIcon
} from "lucide-react"
import { getAllPrompts } from "~libs/db"
export default function OptionLayout({
children
}: {
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const {
selectedModel,
setSelectedModel,
clearChat,
selectedSystemPrompt,
setSelectedQuickPrompt,
setSelectedSystemPrompt
} = useMessageOption()
const {
data: models,
isLoading: isModelsLoading,
isFetching: isModelsFetching
} = useQuery({
queryKey: ["fetchModel"],
queryFn: () => getAllModels({ returnEmpty: true }),
refetchInterval: 15000
})
const { data: prompts, isLoading: isPromptLoading } = useQuery({
queryKey: ["fetchAllPromptsLayout"],
queryFn: getAllPrompts
})
const { pathname } = useLocation()
const getPromptInfoById = (id: string) => {
return prompts?.find((prompt) => prompt.id === id)
}
const handlePromptChange = (value: string) => {
const prompt = getPromptInfoById(value)
if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id)
} else {
setSelectedQuickPrompt(prompt.content)
setSelectedSystemPrompt(null)
}
}
return (
<div>
<div>
<div className="flex flex-col">
<div className="sticky top-0 z-[999] flex h-16 p-3 bg-white border-b border-gray-200 dark:bg-[#171717] dark:border-gray-600">
<div className="flex gap-2 items-center">
{pathname !== "/" && (
<div>
<NavLink
to="/"
className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<ChevronLeft className="w-6 h-6" />
</NavLink>
</div>
)}
<div>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => setSidebarOpen(true)}>
<PanelLeftIcon className="w-6 h-6" />
</button>
</div>
<div>
<button
onClick={clearChat}
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 shadow-sm dark:text-white disabled:opacity-50 ">
<SquarePen className="h-4 w-4 mr-3" />
New Chat
</button>
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"}
</span>
<div>
<Select
value={selectedModel}
onChange={setSelectedModel}
size="large"
loading={isModelsLoading || isModelsFetching}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
showSearch
placeholder="Select a model"
className="w-64 "
options={models?.map((model) => ({
label: model.name,
value: model.model
}))}
/>
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"}
</span>
<div>
<Select
size="large"
loading={isPromptLoading}
showSearch
placeholder="Select a prompt"
className="w-60"
allowClear
onChange={handlePromptChange}
value={selectedSystemPrompt}
filterOption={(input, option) =>
option.label.key
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
options={prompts?.map((prompt) => ({
label: (
<span
key={prompt.title}
className="flex flex-row justify-between items-center">
{prompt.title}
{prompt.is_system ? (
<ComputerIcon className="w-4 h-4" />
) : (
<ZapIcon className="w-4 h-4" />
)}
</span>
),
value: prompt.id
}))}
/>
</div>
</div>
<div className="flex flex-1 justify-end px-4">
<div className="ml-4 flex items-center md:ml-6">
<div className="flex gap-4 items-center">
{/* <Tooltip title="Manage Prompts">
<NavLink
to="/prompts"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Book className="w-6 h-6" />
</NavLink>
</Tooltip> */}
<Tooltip title="Github Repository">
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
{/* <Tooltip title="Manage Ollama Models">
<NavLink
to="/models"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<BrainCircuit className="w-6 h-6" />
</NavLink>
</Tooltip> */}
<Tooltip title="Manage Ollama Models">
<NavLink
to="/settings"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<CogIcon className="w-6 h-6" />
</NavLink>
</Tooltip>
</div>
</div>
</div>
</div>
<main className="flex-1">{children}</main>
</div>
</div>
<Drawer
title={"Chat History"}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar />
</Drawer>
</div>
)
}

View File

@ -0,0 +1,90 @@
import {
Book,
BrainCircuit,
CircuitBoardIcon,
Orbit
} from "lucide-react"
import { Link, useLocation } from "react-router-dom"
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ")
}
const LinkComponent = (item: {
href: string
name: string
icon: any
current: string
}) => {
return (
<li>
<Link
to={item.href}
className={classNames(
item.current === item.href
? "bg-gray-100 text-indigo-600 dark:bg-[#262626] dark:text-white"
: "text-gray-700 hover:text-indigo-600 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-[#262626]",
"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm leading-6 font-semibold"
)}>
<item.icon
className={classNames(
item.current === item.href
? "text-indigo-600 dark:text-white"
: "text-gray-400 group-hover:text-indigo-600 dark:text-gray-200 dark:group-hover:text-white",
"h-6 w-6 shrink-0"
)}
aria-hidden="true"
/>
{item.name}
</Link>
</li>
)
}
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
const location = useLocation()
return (
<>
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
<aside className="flex lg:rounded-md bg-white lg:h-56 lg:p-4 lg:mt-20 overflow-x-auto lg:border border-b py-4 lg:block lg:w-64 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
<nav className="flex-none px-4 sm:px-6 lg:px-0">
<ul
role="list"
className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col">
<LinkComponent
href="/settings"
name="General Settings"
icon={Orbit}
current={location.pathname}
/>
<LinkComponent
href="/settings/ollama"
name="Ollama Settings"
icon={CircuitBoardIcon}
current={location.pathname}
/>
<LinkComponent
href="/settings/model"
name="Manage Model"
current={location.pathname}
icon={BrainCircuit}
/>
<LinkComponent
href="/settings/prompt"
name="Manage Prompt"
icon={Book}
current={location.pathname}
/>
</ul>
</nav>
</aside>
<main className={"px-4 py-16 sm:px-6 lg:flex-auto lg:px-0 lg:py-20"}>
<div className="mx-auto max-w-2xl space-y-16 sm:space-y-10 lg:mx-0 lg:max-w-none">
{children}
</div>
</main>
</div>
</>
)
}

View File

@ -1,131 +0,0 @@
import React, { useState } from "react"
import { useLocation, NavLink } from "react-router-dom"
import { Sidebar } from "./Sidebar"
import { Drawer, Layout, Modal, Select, Tooltip } from "antd"
import { useQuery } from "@tanstack/react-query"
import { getAllModels } from "~services/ollama"
import { useMessageOption } from "~hooks/useMessageOption"
import { Settings } from "./Settings"
import { BrainCircuit, ChevronLeft, CogIcon, GithubIcon, PanelLeftIcon, SquarePen } from "lucide-react"
export default function OptionLayout({
children
}: {
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const [open, setOpen] = useState(false)
const {
data: models,
isLoading: isModelsLoading,
isFetching: isModelsFetching
} = useQuery({
queryKey: ["fetchModel"],
queryFn: getAllModels,
refetchInterval: 15000
})
const { pathname } = useLocation()
const { selectedModel, setSelectedModel, clearChat } = useMessageOption()
return (
<Layout className="bg-white dark:bg-[#171717] md:flex">
<div className="flex items-center p-3 fixed flex-row justify-between border-b border-gray-200 dark:border-gray-600 bg-white dark:bg-[#171717] w-full z-10">
<div className="flex items-center flex-row gap-3">
{pathname !== "/" && (
<div>
<NavLink
to="/"
className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<ChevronLeft className="w-6 h-6" />
</NavLink>
</div>
)}
<div>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => setSidebarOpen(true)}>
<PanelLeftIcon className="w-6 h-6" />
</button>
</div>
<div>
<button
onClick={clearChat}
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 shadow-sm dark:text-white disabled:opacity-50 ">
<SquarePen className="h-4 w-4 mr-3" />
New Chat
</button>
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"}
</span>
<div>
<Select
value={selectedModel}
onChange={setSelectedModel}
size="large"
loading={isModelsLoading || isModelsFetching}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
showSearch
placeholder="Select a model"
className="w-64 "
options={models?.map((model) => ({
label: model.name,
value: model.model
}))}
/>
</div>
</div>
<div className="flex gap-4 items-center">
<Tooltip title="Github Repository">
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
<Tooltip title="Manage Ollama Models">
<NavLink
to="/models"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<BrainCircuit className="w-6 h-6" />
</NavLink>
</Tooltip>
<button
onClick={() => setOpen(true)}
className="text-gray-500 dark:text-gray-400">
<CogIcon className="w-6 h-6" />
</button>
</div>
</div>
<Layout.Content>{children}</Layout.Content>
<Drawer
title={"Chat History"}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar />
</Drawer>
<Modal
open={open}
width={800}
title={"Settings"}
onOk={() => setOpen(false)}
footer={null}
onCancel={() => setOpen(false)}>
<Settings setClose={() => setOpen(false)} />
</Modal>
</Layout>
)
}

View File

@ -22,7 +22,7 @@ export const ModelsBody = () => {
const { data, status } = useQuery({
queryKey: ["fetchAllModels"],
queryFn: getAllModels
queryFn: () => getAllModels({ returnEmpty: true })
})
const { mutate: deleteOllamaModel } = useMutation({
@ -67,15 +67,15 @@ export const ModelsBody = () => {
})
return (
<div className="z-10 min-h-screen">
<div className="mt-16 mx-auto py-10 max-w-7xl px-3 sm:px-6 lg:px-8">
<div>
<div>
{/* Add new model button */}
<div className="mb-6">
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap">
<div className="ml-4 mt-2 flex-shrink-0">
<button
onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-3 py-3 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Model
</button>
</div>
@ -212,7 +212,7 @@ export const ModelsBody = () => {
<button
type="submit"
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<Download className="w-5 h-5 mr-3" />
Pull Model
</button>

View File

@ -70,7 +70,7 @@ export const Playground = () => {
ref={drop}
className={`${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800 z-10" : ""
} min-h-screen`}>
} bg-white dark:bg-[#171717]`}>
<PlaygroundChat />
<div className="flex flex-col items-center">
<div className="flex-grow">
@ -78,9 +78,7 @@ export const Playground = () => {
<div className="bottom-0 w-full bg-transparent border-0 fixed pt-2">
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 lg:mx-auto lg:max-w-2xl xl:max-w-3xl justify-center items-center">
<div className="relative h-full flex-1 items-center justify-center md:flex-col">
<PlaygroundForm
dropedFile={dropedFile}
/>
<PlaygroundForm dropedFile={dropedFile} />
</div>
</div>
</div>

View File

@ -18,7 +18,7 @@ export const PlaygroundChat = () => {
<PlaygroundEmpty />
</div>
)}
{messages.length > 0 && <div className="w-full h-16 flex-shrink-0"></div>}
{/* {messages.length > 0 && <div className="w-full h-16 flex-shrink-0"></div>} */}
{messages.map((message, index) => (
<PlaygroundMessage
key={index}

View File

@ -10,19 +10,35 @@ import { useSpeechRecognition } from "~hooks/useSpeechRecognition"
import { useWebUI } from "~store/webui"
import { defaultEmbeddingModelForRag } from "~services/ollama"
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
import { getVariable } from "~utils/select-varaible"
type Props = {
dropedFile: File | undefined
}
export const PlaygroundForm = ({ dropedFile }: Props) => {
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const inputRef = React.useRef<HTMLInputElement>(null)
const {
onSubmit,
selectedModel,
chatMode,
speechToTextLanguage,
stopStreamingRequest,
streaming: isSending,
webSearch,
setWebSearch,
selectedQuickPrompt,
textareaRef,
setSelectedQuickPrompt
} = useMessageOption()
const resetHeight = () => {
const textarea = textareaRef.current
if (textarea) {
textarea.style.height = "auto"
const textAreaFocus = () => {
if (textareaRef.current) {
if (
textareaRef.current.selectionStart === textareaRef.current.selectionEnd
) {
textareaRef.current.focus()
}
}
}
const form = useForm({
@ -33,9 +49,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
})
React.useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus()
}
textAreaFocus()
}, [])
const onInputChange = async (
@ -60,17 +74,6 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
useDynamicTextareaSize(textareaRef, form.values.message, 300)
const {
onSubmit,
selectedModel,
chatMode,
speechToTextLanguage,
stopStreamingRequest,
streaming: isSending,
webSearch,
setWebSearch
} = useMessageOption()
const { isListening, start, stop, transcript } = useSpeechRecognition()
const { sendWhenEnter, setSendWhenEnter } = useWebUI()
@ -80,17 +83,75 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}, [transcript])
React.useEffect(() => {
if (selectedQuickPrompt) {
const word = getVariable(selectedQuickPrompt)
form.setFieldValue("message", selectedQuickPrompt)
if (word) {
textareaRef.current?.focus()
const interval = setTimeout(() => {
textareaRef.current?.setSelectionRange(word.start, word.end)
setSelectedQuickPrompt(null)
}, 100)
return () => {
clearInterval(interval)
}
}
}
}, [selectedQuickPrompt])
const queryClient = useQueryClient()
const { mutateAsync: sendMessage } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
textAreaFocus()
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
},
onError: (error) => {
textAreaFocus()
}
})
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Process" || e.key === "229") return
if (
e.key === "Enter" &&
!e.shiftKey &&
!isSending &&
sendWhenEnter &&
!e.isComposing
) {
e.preventDefault()
form.onSubmit(async (value) => {
if (value.message.trim().length === 0) {
return
}
if (!selectedModel || selectedModel.length === 0) {
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()
textAreaFocus()
await sendMessage({
image: value.image,
message: value.message.trim()
})
})()
}
}
return (
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600">
<div
@ -133,7 +194,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}
form.reset()
resetHeight()
textAreaFocus()
await sendMessage({
image: value.image,
message: value.message.trim()
@ -152,41 +213,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
/>
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
<textarea
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!isSending &&
sendWhenEnter
) {
e.preventDefault()
form.onSubmit(async (value) => {
if (value.message.trim().length === 0) {
return
}
if (!selectedModel || selectedModel.length === 0) {
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({
image: value.image,
message: value.message.trim()
})
})()
}
}}
onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}
ref={textareaRef}
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

View File

@ -0,0 +1,279 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
Skeleton,
Table,
Tooltip,
notification,
Modal,
Input,
Form,
Switch
} from "antd"
import { Trash2, Pen, Computer, Zap } from "lucide-react"
import { useState } from "react"
import {
deletePromptById,
getAllPrompts,
savePrompt,
updatePrompt
} from "~libs/db"
export const PromptBody = () => {
const queryClient = useQueryClient()
const [open, setOpen] = useState(false)
const [openEdit, setOpenEdit] = useState(false)
const [editId, setEditId] = useState("")
const [createForm] = Form.useForm()
const [editForm] = Form.useForm()
const { data, status } = useQuery({
queryKey: ["fetchAllPrompts"],
queryFn: getAllPrompts
})
const { mutate: deletePrompt } = useMutation({
mutationFn: deletePromptById,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchAllPrompts"]
})
notification.success({
message: "Model Deleted",
description: "Model has been deleted successfully"
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
})
}
})
const { mutate: savePromptMutation, isPending: savePromptLoading } =
useMutation({
mutationFn: savePrompt,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchAllPrompts"]
})
setOpen(false)
createForm.resetFields()
notification.success({
message: "Prompt Added",
description: "Prompt has been added successfully"
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
})
}
})
const { mutate: updatePromptMutation, isPending: isUpdatingPrompt } =
useMutation({
mutationFn: async (data: any) => {
return await updatePrompt({
...data,
id: editId
})
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchAllPrompts"]
})
setOpenEdit(false)
editForm.resetFields()
notification.success({
message: "Prompt Updated",
description: "Prompt has been updated successfully"
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
})
}
})
return (
<div>
<div>
<div className="mb-6">
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap">
<div className="ml-4 mt-2 flex-shrink-0">
<button
onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Prompt
</button>
</div>
</div>
</div>
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
{status === "success" && (
<Table
columns={[
{
title: "Title",
dataIndex: "title",
key: "title"
},
{
title: "Prompt",
dataIndex: "content",
key: "content"
},
{
title: "Prompt Type",
dataIndex: "is_system",
key: "is_system",
render: (is_system) =>
is_system ? (
<span className="flex items-center gap-2">
<Computer className="w-5 h-5 " />
System Prompt
</span>
) : (
<span className="flex items-center gap-2">
<Zap className="w-5 h-5" />
Quick Prompt
</span>
)
},
{
title: "Action",
render: (_, record) => (
<div className="flex gap-4">
<Tooltip title="Delete Prompt">
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this prompt? This action cannot be undone."
)
) {
deletePrompt(record.id)
}
}}
className="text-red-500 dark:text-red-400">
<Trash2 className="w-5 h-5" />
</button>
</Tooltip>
<Tooltip title="Edit Prompt">
<button
onClick={() => {
setEditId(record.id)
editForm.setFieldsValue(record)
setOpenEdit(true)
}}
className="text-gray-500 dark:text-gray-400">
<Pen className="w-5 h-5" />
</button>
</Tooltip>
</div>
)
}
]}
bordered
dataSource={data}
rowKey={(record) => record.id}
/>
)}
</div>
<Modal
title="Add New Prompt"
open={open}
onCancel={() => setOpen(false)}
footer={null}>
<Form
onFinish={(values) => savePromptMutation(values)}
layout="vertical"
form={createForm}>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: "Title is required" }]}>
<Input placeholder="My Awesome Prompt" />
</Form.Item>
<Form.Item
name="content"
label="Prompt"
rules={[{ required: true, message: "Prompt is required" }]}
help="You can use {key} as variable in your prompt.">
<Input.TextArea
placeholder="Your prompt goes here..."
autoSize={{ minRows: 3, maxRows: 10 }}
/>
</Form.Item>
<Form.Item
name="is_system"
label="Is System Prompt"
valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item>
<button
disabled={savePromptLoading}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{savePromptLoading ? "Adding Prompt..." : "Add Prompt"}
</button>
</Form.Item>
</Form>
</Modal>
<Modal
title="Update Prompt"
open={openEdit}
onCancel={() => setOpenEdit(false)}
footer={null}>
<Form
onFinish={(values) => updatePromptMutation(values)}
layout="vertical"
form={editForm}>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: "Title is required" }]}>
<Input placeholder="My Awesome Prompt" />
</Form.Item>
<Form.Item
name="content"
label="Prompt"
rules={[{ required: true, message: "Prompt is required" }]}
help="You can use {key} as variable in your prompt.">
<Input.TextArea
placeholder="Your prompt goes here..."
autoSize={{ minRows: 3, maxRows: 10 }}
/>
</Form.Item>
<Form.Item
name="is_system"
label="Is System Prompt"
valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item>
<button
disabled={isUpdatingPrompt}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{isUpdatingPrompt ? "Updating Prompt..." : "Update Prompt"}
</button>
</Form.Item>
</Form>
</Modal>
</div>
)
}

View File

@ -11,6 +11,7 @@ import {
saveForRag,
setOllamaURL as saveOllamaURL
} from "~services/ollama"
import { SettingPrompt } from "./prompt"
export const SettingsOllama = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
@ -20,7 +21,7 @@ export const SettingsOllama = () => {
const [ollamaURL, allModels, chunkOverlap, chunkSize, defaultEM] =
await Promise.all([
getOllamaURL(),
getAllModels(),
getAllModels({returnEmpty: true}),
defaultEmbeddingChunkOverlap(),
defaultEmbeddingChunkSize(),
defaultEmbeddingModelForRag()
@ -46,10 +47,17 @@ export const SettingsOllama = () => {
})
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col space-y-3">
{status === "pending" && <Skeleton paragraph={{ rows: 4 }} active />}
{status === "success" && (
<>
<div className="flex flex-col space-y-6">
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure Ollama
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
<div>
<label
htmlFor="ollamaURL"
@ -75,7 +83,15 @@ export const SettingsOllama = () => {
className="mt-2"
/>
</div>
</div>
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
<Form
layout="vertical"
onFinish={(data) => {
@ -119,13 +135,19 @@ export const SettingsOllama = () => {
rules={[
{ required: true, message: "Please input your chunk size!" }
]}>
<InputNumber style={{ width: "100%" }} placeholder="Chunk Size" />
<InputNumber
style={{ width: "100%" }}
placeholder="Chunk Size"
/>
</Form.Item>
<Form.Item
name="chunkOverlap"
label="Chunk Overlap"
rules={[
{ required: true, message: "Please input your chunk overlap!" }
{
required: true,
message: "Please input your chunk overlap!"
}
]}>
<InputNumber
style={{ width: "100%" }}
@ -137,7 +159,18 @@ export const SettingsOllama = () => {
<SaveButton disabled={isSaveRAGPending} btnType="submit" />
</div>
</Form>
</>
</div>
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG Prompt
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
<SettingPrompt />
</div>
</div>
)}
</div>
)

View File

@ -5,6 +5,7 @@ import { PageAssitDatabase } from "~libs/db"
import { Select } from "antd"
import { SUPPORTED_LANGUAGES } from "~utils/supporetd-languages"
import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode"
export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -15,9 +16,15 @@ export const SettingOther = () => {
const { mode, toggleDarkMode } = useDarkMode()
return (
<div className="flex flex-col space-y-4">
<dl className="flex flex-col space-y-6">
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Web UI Settings
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-gray-400 text-md">
<span className="text-gray-500 dark:text-gray-400 text-lg">
Speech Recognition Language
</span>
@ -37,7 +44,7 @@ export const SettingOther = () => {
/>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-gray-400 text-md">
<span className="text-gray-500 dark:text-gray-400 text-lg">
Change Theme
</span>
@ -53,7 +60,7 @@ export const SettingOther = () => {
</button>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-gray-400 text-md">
<span className="text-gray-500 dark:text-gray-400 text-lg">
Delete Chat History
</span>
@ -76,6 +83,7 @@ export const SettingOther = () => {
Delete
</button>
</div>
</div>
<SearchModeSettings />
</dl>
)
}

View File

@ -1,5 +1,5 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Radio, Form } from "antd"
import { Skeleton, Radio, Form, Alert } from "antd"
import React from "react"
import { SaveButton } from "~components/Common/SaveButton"
import {
@ -12,7 +12,7 @@ import {
export const SettingPrompt = () => {
const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">(
"normal"
"web"
)
const queryClient = useQueryClient()
@ -41,7 +41,6 @@ export const SettingPrompt = () => {
{status === "success" && (
<div>
<h2 className="text-md font-semibold dark:text-white">Prompt</h2>
<div className="my-3 flex justify-end">
<Radio.Group
defaultValue={selectedValue}
@ -63,6 +62,14 @@ export const SettingPrompt = () => {
initialValues={{
prompt: data.prompt
}}>
<Form.Item>
<Alert
message="Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release"
type="warning"
showIcon
closable
/>
</Form.Item>
<Form.Item label="System Prompt" name="prompt">
<textarea
value={data.prompt}

View File

@ -0,0 +1,37 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Switch } from "antd"
import {
getIsSimpleInternetSearch,
setIsSimpleInternetSearch
} from "~services/ollama"
export const SearchModeSettings = () => {
const { data, status } = useQuery({
queryKey: ["fetchIsSimpleInternetSearch"],
queryFn: () => getIsSimpleInternetSearch()
})
const queryClient = useQueryClient()
if (status === "pending" || status === "error") {
return <Skeleton active />
}
return (
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-gray-400 text-lg">
Perform Simple Internet Search
</span>
<Switch
checked={data}
onChange={(checked) => {
setIsSimpleInternetSearch(checked)
queryClient.invalidateQueries({
queryKey: ["fetchIsSimpleInternetSearch"]
})
}}
/>
</div>
)
}

View File

@ -22,7 +22,7 @@ export const EmptySidePanel = () => {
queryFn: async () => {
const ollamaURL = await getOllamaURL()
const isOk = await isOllamaRunning()
const models = await getAllModels()
const models = await getAllModels({ returnEmpty: false })
return {
isOk,

View File

@ -19,10 +19,9 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
const inputRef = React.useRef<HTMLInputElement>(null)
const { sendWhenEnter, setSendWhenEnter } = useWebUI()
const resetHeight = () => {
const textarea = textareaRef.current
if (textarea) {
textarea.style.height = "auto"
const textAreaFocus = () => {
if (textareaRef.current) {
textareaRef.current.focus()
}
}
const form = useForm({
@ -64,9 +63,53 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
}
}, [transcript])
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
mutationFn: onSubmit
mutationFn: onSubmit,
onSuccess: () => {
textAreaFocus()
},
onError: (error) => {
textAreaFocus()
}
})
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Process" || e.key === "229") return
if (
e.key === "Enter" &&
!e.shiftKey &&
!isSending &&
sendWhenEnter &&
!e.isComposing
) {
e.preventDefault()
form.onSubmit(async (value) => {
if (value.message.trim().length === 0) {
return
}
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
return
}
if (chatMode === "rag") {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the settings page"
)
return
}
}
form.reset()
textAreaFocus()
await sendMessage({
image: value.image,
message: value.message.trim()
})
})()
}
}
return (
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600">
<div
@ -109,7 +152,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
}
}
form.reset()
resetHeight()
textAreaFocus()
await sendMessage({
image: value.image,
message: value.message.trim()
@ -128,41 +171,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
/>
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
<textarea
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!isSending &&
sendWhenEnter
) {
e.preventDefault()
form.onSubmit(async (value) => {
if (value.message.trim().length === 0) {
return
}
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
return
}
if (chatMode === "rag") {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the settings page"
)
return
}
}
form.reset()
resetHeight()
await sendMessage({
image: value.image,
message: value.message.trim()
})
})()
}
}}
onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}
ref={textareaRef}
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
@ -211,9 +220,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
</Tooltip>
<Dropdown.Button
htmlType="submit"
disabled={
isSending
}
disabled={isSending}
className="!justify-end !w-auto"
icon={
<svg

View File

@ -48,7 +48,7 @@ export const SettingsBody = () => {
getOllamaURL(),
systemPromptForNonRag(),
promptForRag(),
getAllModels(),
getAllModels({ returnEmpty: true }),
defaultEmbeddingChunkOverlap(),
defaultEmbeddingChunkSize(),
defaultEmbeddingModelForRag()

View File

@ -250,7 +250,6 @@ export const useMessage = () => {
setIsProcessing(false)
} catch (e) {
console.log(e)
setIsProcessing(false)
setStreaming(false)
@ -388,7 +387,6 @@ ${e?.message}
setIsProcessing(false)
} catch (e) {
console.log(e)
setIsProcessing(false)
setStreaming(false)

View File

@ -14,7 +14,12 @@ import {
SystemMessage
} from "@langchain/core/messages"
import { useStoreMessageOption } from "~store/option"
import { removeMessageUsingHistoryId, saveHistory, saveMessage } from "~libs/db"
import {
getPromptById,
removeMessageUsingHistoryId,
saveHistory,
saveMessage
} from "~libs/db"
import { useNavigate } from "react-router-dom"
import { notification } from "antd"
import { getSystemPromptForWeb } from "~web/web"
@ -102,14 +107,20 @@ export const useMessageOption = () => {
webSearch,
setWebSearch,
isSearchingInternet,
setIsSearchingInternet
setIsSearchingInternet,
selectedQuickPrompt,
setSelectedQuickPrompt,
selectedSystemPrompt,
setSelectedSystemPrompt
} = useStoreMessageOption()
const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const abortControllerRef = React.useRef<AbortController | null>(null)
const clearChat = () => {
navigate("/")
setMessages([])
setHistory([])
setHistoryId(null)
@ -117,7 +128,7 @@ export const useMessageOption = () => {
setIsLoading(false)
setIsProcessing(false)
setStreaming(false)
navigate("/")
textareaRef?.current?.focus()
}
const searchChatMode = async (
@ -310,7 +321,7 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
} catch (e) {
console.log(e)
e
if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[
@ -406,6 +417,7 @@ export const useMessageOption = () => {
try {
const prompt = await systemPromptForNonRagOption()
const selectedPrompt = await getPromptById(selectedSystemPrompt)
message = message.trim().replaceAll("\n", " ")
@ -434,7 +446,7 @@ export const useMessageOption = () => {
const applicationChatHistory = generateHistory(history)
if (prompt) {
if (prompt && !selectedPrompt) {
applicationChatHistory.unshift(
new SystemMessage({
content: [
@ -447,6 +459,19 @@ export const useMessageOption = () => {
)
}
if (selectedPrompt) {
applicationChatHistory.unshift(
new SystemMessage({
content: [
{
text: selectedPrompt.content,
type: "text"
}
]
})
)
}
const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage],
{
@ -526,8 +551,6 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
} catch (e) {
console.log(e)
if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[
appendingIndex
@ -645,6 +668,12 @@ export const useMessageOption = () => {
regenerateLastMessage,
webSearch,
setWebSearch,
isSearchingInternet
isSearchingInternet,
setIsSearchingInternet,
selectedQuickPrompt,
setSelectedQuickPrompt,
selectedSystemPrompt,
setSelectedSystemPrompt,
textareaRef
}
}

View File

@ -1,176 +1,172 @@
import { useRef, useEffect, useState, useCallback } from "react";
import { useRef, useEffect, useState, useCallback } from "react"
type SpeechRecognitionEvent = {
results: SpeechRecognitionResultList;
resultIndex: number;
};
results: SpeechRecognitionResultList
resultIndex: number
}
declare global {
interface SpeechRecognitionErrorEvent extends Event {
error: string;
error: string
}
interface Window {
SpeechRecognition: any;
webkitSpeechRecognition: any;
SpeechRecognition: any
webkitSpeechRecognition: any
}
}
type SpeechRecognition = {
lang: string;
interimResults: boolean;
continuous: boolean;
maxAlternatives: number;
grammars: any;
onresult: (event: SpeechRecognitionEvent) => void;
onerror: (event: Event) => void;
onend: () => void;
start: () => void;
stop: () => void;
};
lang: string
interimResults: boolean
continuous: boolean
maxAlternatives: number
grammars: any
onresult: (event: SpeechRecognitionEvent) => void
onerror: (event: Event) => void
onend: () => void
start: () => void
stop: () => void
}
type SpeechRecognitionProps = {
onEnd?: () => void;
onResult?: (transcript: string) => void;
onError?: (event: Event) => void;
};
onEnd?: () => void
onResult?: (transcript: string) => void
onError?: (event: Event) => void
}
type ListenArgs = {
lang?: string;
interimResults?: boolean;
continuous?: boolean;
maxAlternatives?: number;
grammars?: any;
};
lang?: string
interimResults?: boolean
continuous?: boolean
maxAlternatives?: number
grammars?: any
}
type SpeechRecognitionHook = {
start: (args?: ListenArgs) => void;
isListening: boolean;
stop: () => void;
supported: boolean;
transcript: string;
};
start: (args?: ListenArgs) => void
isListening: boolean
stop: () => void
supported: boolean
transcript: string
}
const useEventCallback = <T extends (...args: any[]) => any>(
fn: T,
dependencies: any[]
) => {
const ref = useRef<T>();
const ref = useRef<T>()
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
ref.current = fn
}, [fn, ...dependencies])
return useCallback(
(...args: Parameters<T>) => {
const fn = ref.current;
return fn!(...args);
const fn = ref.current
return fn!(...args)
},
[ref]
);
};
)
}
export const useSpeechRecognition = (
props: SpeechRecognitionProps = {}
): SpeechRecognitionHook => {
const { onEnd = () => {}, onResult = () => {}, onError = () => {} } = props;
const recognition = useRef<SpeechRecognition | null>(null);
const [listening, setListening] = useState<boolean>(false);
const [supported, setSupported] = useState<boolean>(false);
const [liveTranscript, setLiveTranscript] = useState<string>("");
const { onEnd = () => {}, onResult = () => {}, onError = () => {} } = props
const recognition = useRef<SpeechRecognition | null>(null)
const [listening, setListening] = useState<boolean>(false)
const [supported, setSupported] = useState<boolean>(false)
const [liveTranscript, setLiveTranscript] = useState<string>("")
useEffect(() => {
if (typeof window === "undefined") return;
if (typeof window === "undefined") return
window.SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
console.log("window.SpeechRecognition", window.SpeechRecognition);
window.SpeechRecognition || window.webkitSpeechRecognition
if (window.SpeechRecognition) {
setSupported(true);
recognition.current = new window.SpeechRecognition();
setSupported(true)
recognition.current = new window.SpeechRecognition()
}
}, []);
}, [])
const processResult = (event: SpeechRecognitionEvent) => {
const transcript = Array.from(event.results)
.map((result) => result[0])
.map((result) => result.transcript)
.join("");
.join("")
onResult(transcript);
};
onResult(transcript)
}
const handleError = (event: Event) => {
if ((event as SpeechRecognitionErrorEvent).error === "not-allowed") {
if (recognition.current) {
recognition.current.onend = null;
recognition.current.onend = null
}
setListening(false);
setListening(false)
}
onError(event)
}
onError(event);
};
const listen = useEventCallback(
(args: ListenArgs = {}) => {
if (listening || !supported) return;
if (listening || !supported) return
const {
lang = "",
interimResults = true,
continuous = false,
maxAlternatives = 1,
grammars,
} = args;
setListening(true);
setLiveTranscript("");
grammars
} = args
setListening(true)
setLiveTranscript("")
if (recognition.current) {
recognition.current.lang = lang;
recognition.current.interimResults = interimResults;
recognition.current.lang = lang
recognition.current.interimResults = interimResults
recognition.current.onresult = (event) => {
processResult(event);
processResult(event)
const transcript = Array.from(event.results)
.map((result) => result[0])
.map((result) => result.transcript)
.join("");
setLiveTranscript(transcript);
};
recognition.current.onerror = handleError;
recognition.current.continuous = continuous;
recognition.current.maxAlternatives = maxAlternatives;
.join("")
setLiveTranscript(transcript)
}
recognition.current.onerror = handleError
recognition.current.continuous = continuous
recognition.current.maxAlternatives = maxAlternatives
if (grammars) {
recognition.current.grammars = grammars;
recognition.current.grammars = grammars
}
recognition.current.onend = () => {
if (recognition.current) {
recognition.current.start();
recognition.current.start()
}
}
};
if (recognition.current) {
recognition.current.start();
recognition.current.start()
}
}
},
[listening, supported, recognition]
);
)
const stop = useEventCallback(() => {
if (!listening || !supported) return;
if (!listening || !supported) return
if (recognition.current) {
recognition.current.onresult = null;
recognition.current.onend = null;
recognition.current.onerror = null;
setListening(false);
recognition.current.stop();
recognition.current.onresult = null
recognition.current.onend = null
recognition.current.onerror = null
setListening(false)
recognition.current.stop()
}
onEnd();
}, [listening, supported, recognition, onEnd]);
onEnd()
}, [listening, supported, recognition, onEnd])
return {
start: listen,
isListening: listening,
stop,
supported,
transcript: liveTranscript,
};
};
transcript: liveTranscript
}
}

View File

@ -31,10 +31,22 @@ type Message = {
createdAt: number
}
type Prompt = {
id: string
title: string
content: string
is_system: boolean
createdBy?: string
createdAt: number
}
type MessageHistory = Message[]
type ChatHistory = HistoryInfo[]
type Prompts = Prompt[]
export class PageAssitDatabase {
db: chrome.storage.StorageArea
@ -102,8 +114,49 @@ export class PageAssitDatabase {
async deleteMessage(history_id: string) {
await this.db.remove(history_id)
}
async getAllPrompts(): Promise<Prompts> {
return new Promise((resolve, reject) => {
this.db.get("prompts", (result) => {
resolve(result.prompts || [])
})
})
}
async addPrompt(prompt: Prompt) {
const prompts = await this.getAllPrompts()
const newPrompts = [prompt, ...prompts]
this.db.set({ prompts: newPrompts })
}
async deletePrompt(id: string) {
const prompts = await this.getAllPrompts()
const newPrompts = prompts.filter((prompt) => prompt.id !== id)
this.db.set({ prompts: newPrompts })
}
async updatePrompt(id: string, title: string, content: string, is_system: boolean) {
const prompts = await this.getAllPrompts()
const newPrompts = prompts.map((prompt) => {
if (prompt.id === id) {
prompt.title = title
prompt.content = content
prompt.is_system = is_system
}
return prompt
})
this.db.set({ prompts: newPrompts })
}
async getPromptById(id: string) {
const prompts = await this.getAllPrompts()
return prompts.find((prompt) => prompt.id === id)
}
}
const generateID = () => {
return "pa_xxxx-xxxx-xxx-xxxx".replace(/[x]/g, () => {
const r = Math.floor(Math.random() * 16)
@ -188,3 +241,40 @@ export const removeMessageUsingHistoryId = async (history_id: string) => {
const newChatHistory = chatHistory.slice(0, -1)
await db.db.set({ [history_id]: newChatHistory })
}
export const getAllPrompts = async () => {
const db = new PageAssitDatabase()
return await db.getAllPrompts()
}
export const savePrompt = async ({ content, title, is_system = false }: { title: string, content: string, is_system: boolean }) => {
const db = new PageAssitDatabase()
const id = generateID()
const createdAt = Date.now()
const prompt = { id, title, content, is_system, createdAt }
await db.addPrompt(prompt)
return prompt
}
export const deletePromptById = async (id: string) => {
const db = new PageAssitDatabase()
await db.deletePrompt(id)
return id
}
export const updatePrompt = async ({ content, id, title, is_system }: { id: string, title: string, content: string, is_system: boolean }) => {
const db = new PageAssitDatabase()
await db.updatePrompt(id, title, content, is_system)
return id
}
export const getPromptById = async (id: string) => {
if (!id || id.trim() === "") return null
const db = new PageAssitDatabase()
return await db.getPromptById(id)
}

8
src/options.html Normal file
View File

@ -0,0 +1,8 @@
<!doctype html>
<html>
<head>
<title>__plasmo_static_index_title__</title>
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]"></body>
</html>

View File

@ -3,7 +3,10 @@ import { SidepanelChat } from "./sidepanel-chat"
import { useDarkMode } from "~hooks/useDarkmode"
import { SidepanelSettings } from "./sidepanel-settings"
import { OptionIndex } from "./option-index"
import { OptionModal } from "./option-model"
import { OptionModal } from "./option-settings-model"
import { OptionPrompt } from "./option-settings-prompt"
import { OptionOllamaSettings } from "./options-settings-ollama"
import { OptionSettings } from "./option-settings"
export const OptionRouting = () => {
const { mode } = useDarkMode()
@ -12,7 +15,10 @@ export const OptionRouting = () => {
<div className={mode === "dark" ? "dark" : "light"}>
<Routes>
<Route path="/" element={<OptionIndex />} />
<Route path="/models" element={<OptionModal />} />
<Route path="/settings" element={<OptionSettings />} />
<Route path="/settings/model" element={<OptionModal />} />
<Route path="/settings/prompt" element={<OptionPrompt />} />
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
</Routes>
</div>
)

View File

@ -1,4 +1,4 @@
import OptionLayout from "~components/Option/Layout"
import OptionLayout from "~components/Layouts/Layout"
import { Playground } from "~components/Option/Playground/Playground"
export const OptionIndex = () => {

View File

@ -1,10 +0,0 @@
import OptionLayout from "~components/Option/Layout"
import { ModelsBody } from "~components/Option/Models"
export const OptionModal = () => {
return (
<OptionLayout>
<ModelsBody />
</OptionLayout>
)
}

View File

@ -0,0 +1,13 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { ModelsBody } from "~components/Option/Models"
export const OptionModal = () => {
return (
<OptionLayout>
<SettingsLayout>
<ModelsBody />
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -0,0 +1,13 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { PromptBody } from "~components/Option/Prompt"
export const OptionPrompt = () => {
return (
<OptionLayout>
<SettingsLayout>
<PromptBody />
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -0,0 +1,13 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { SettingOther } from "~components/Option/Settings/other"
export const OptionSettings = () => {
return (
<OptionLayout>
<SettingsLayout>
<SettingOther />
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -0,0 +1,13 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { SettingsOllama } from "~components/Option/Settings/ollama"
export const OptionOllamaSettings = () => {
return (
<OptionLayout>
<SettingsLayout>
<SettingsOllama />
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -60,10 +60,13 @@ export const isOllamaRunning = async () => {
}
}
export const getAllModels = async () => {
export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
if (returnEmpty) {
return []
}
throw new Error(response.statusText)
}
const json = await response.json()
@ -286,3 +289,17 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => {
await setWebSearchPrompt(prompt)
await setWebSearchFollowUpPrompt(followUpPrompt)
}
export const getIsSimpleInternetSearch = async () => {
const isSimpleInternetSearch = await storage.get("isSimpleInternetSearch")
if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) {
return true
}
return isSimpleInternetSearch === "true"
}
export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => {
await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString())
}

View File

@ -51,6 +51,12 @@ type State = {
setWebSearch: (webSearch: boolean) => void;
isSearchingInternet: boolean;
setIsSearchingInternet: (isSearchingInternet: boolean) => void;
selectedSystemPrompt: string | null
setSelectedSystemPrompt: (selectedSystemPrompt: string) => void
selectedQuickPrompt: string | null
setSelectedQuickPrompt: (selectedQuickPrompt: string) => void
}
export const useStoreMessageOption = create<State>((set) => ({
@ -81,4 +87,8 @@ export const useStoreMessageOption = create<State>((set) => ({
setWebSearch: (webSearch) => set({ webSearch }),
isSearchingInternet: false,
setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),
selectedSystemPrompt: null,
setSelectedSystemPrompt: (selectedSystemPrompt) => set({ selectedSystemPrompt }),
selectedQuickPrompt: null,
setSelectedQuickPrompt: (selectedQuickPrompt) => set({ selectedQuickPrompt }),
}))

View File

@ -0,0 +1,24 @@
export const getVariable = (text: string) => {
const regex = /{([^}]+)}/g;
let data : {
word: string,
start: number,
end: number
} | null = null;
let m: RegExpExecArray | null;
while ((m = regex.exec(text)) !== null) {
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
data = {
word: m[1],
start: m.index,
end: m.index + m[0].length
}
}
return data;
}

View File

@ -5,7 +5,7 @@ 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"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~services/ollama"
const BLOCKED_HOSTS = [
"google.com",
@ -40,7 +40,8 @@ export const localGoogleSearch = async (query: string) => {
(result) => {
const title = result.querySelector("h3")?.textContent
const link = result.querySelector("a")?.getAttribute("href")
return { title, link }
const content = Array.from(result.querySelectorAll("span")).map((span) => span.textContent).join(" ")
return { title, link, content }
}
)
const filteredSearchResults = searchResults
@ -58,6 +59,18 @@ export const webSearch = async (query: string) => {
const results = await localGoogleSearch(query)
const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)
const isSimpleMode = await getIsSimpleInternetSearch()
if (isSimpleMode) {
await getOllamaURL()
return searchResults.map((result) => {
return {
url: result.link,
content: result.content
}
})
}
const docs: Document<Record<string, any>>[] = [];
for (const result of searchResults) {
const loader = new PageAssistHtmlLoader({

View File

@ -2456,11 +2456,6 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
"@types/pdf-parse@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1"
integrity sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==
"@types/prop-types@*":
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
@ -7631,11 +7626,6 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
voy-search@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/voy-search/-/voy-search-0.6.3.tgz#5fed7744aac990c99a57b88bf4e7431ee6ba9ddd"
integrity sha512-GRwrXcT3Qmzr/CuwpwX55XWpgqM2hUqLipSwI8bGcfsDTJGa+mFxsOXzWHNMRpcYd+U2RP73f2USLDWQu5yFdQ==
vue@3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"