commit
						77c1fd6e3e
					
				| @ -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": { | ||||
|  | ||||
| @ -96,7 +96,6 @@ chrome.runtime.onMessage.addListener(async (message) => { | ||||
|         clearBadge() | ||||
|       }, 5000) | ||||
|     } | ||||
|     console.log("Pulling model", message.modelName) | ||||
| 
 | ||||
|     await streamDownload(ollamaURL, message.modelName) | ||||
|   } | ||||
|  | ||||
							
								
								
									
										205
									
								
								src/components/Layouts/Layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								src/components/Layouts/Layout.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										90
									
								
								src/components/Layouts/SettingsOptionLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/components/Layouts/SettingsOptionLayout.tsx
									
									
									
									
									
										Normal 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> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| @ -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> | ||||
|   ) | ||||
| } | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										279
									
								
								src/components/Option/Prompt/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								src/components/Option/Prompt/index.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
| @ -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> | ||||
|   ) | ||||
|  | ||||
| @ -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> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
							
								
								
									
										37
									
								
								src/components/Option/Settings/search-mode.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/components/Option/Settings/search-mode.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
| @ -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, | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -48,7 +48,7 @@ export const SettingsBody = () => { | ||||
|         getOllamaURL(), | ||||
|         systemPromptForNonRag(), | ||||
|         promptForRag(), | ||||
|         getAllModels(), | ||||
|         getAllModels({ returnEmpty: true }), | ||||
|         defaultEmbeddingChunkOverlap(), | ||||
|         defaultEmbeddingChunkSize(), | ||||
|         defaultEmbeddingModelForRag() | ||||
|  | ||||
| @ -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) | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										8
									
								
								src/options.html
									
									
									
									
									
										Normal 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> | ||||
| @ -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> | ||||
|   ) | ||||
|  | ||||
| @ -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 = () => { | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| import OptionLayout from "~components/Option/Layout" | ||||
| import { ModelsBody } from "~components/Option/Models" | ||||
| 
 | ||||
| export const OptionModal = () => { | ||||
|   return ( | ||||
|     <OptionLayout> | ||||
|       <ModelsBody /> | ||||
|     </OptionLayout> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/routes/option-settings-model.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/option-settings-model.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/routes/option-settings-prompt.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/option-settings-prompt.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/routes/option-settings.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/option-settings.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/routes/options-settings-ollama.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/options-settings-ollama.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ) | ||||
| } | ||||
| @ -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()) | ||||
| } | ||||
| @ -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 }), | ||||
| })) | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/utils/select-varaible.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/utils/select-varaible.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @ -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({ | ||||
|  | ||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -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" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user