feat: copilot context menu for tool
This commit is contained in:
		
							parent
							
								
									9c7ebc8778
								
							
						
					
					
						commit
						ac9c9ca887
					
				| @ -1,6 +1,6 @@ | ||||
| import Markdown from "../../Common/Markdown" | ||||
| import React from "react" | ||||
| import { Image, Tooltip } from "antd" | ||||
| import { Tag, Image, Tooltip } from "antd" | ||||
| import { WebSearch } from "./WebSearch" | ||||
| import { | ||||
|   CheckIcon, | ||||
| @ -17,6 +17,7 @@ import { useTTS } from "@/hooks/useTTS" | ||||
| 
 | ||||
| type Props = { | ||||
|   message: string | ||||
|   message_type?: string | ||||
|   hideCopy?: boolean | ||||
|   botAvatar?: JSX.Element | ||||
|   userAvatar?: JSX.Element | ||||
| @ -76,13 +77,21 @@ export const PlaygroundMessage = (props: Props) => { | ||||
|             props.currentMessageIndex === props.totalMessages - 1 ? ( | ||||
|               <WebSearch /> | ||||
|             ) : null} | ||||
| 
 | ||||
|             <div> | ||||
|               {props?.message_type && ( | ||||
|                 <Tag color="blue">{props?.message_type}</Tag> | ||||
|               )} | ||||
|             </div> | ||||
|             <div className="flex flex-grow flex-col"> | ||||
|               {!editMode ? ( | ||||
|                 props.isBot ? ( | ||||
|                   <Markdown message={props.message} /> | ||||
|                 ) : ( | ||||
|                   <p className="prose dark:prose-invert whitespace-pre-line	 prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"> | ||||
|                   <p | ||||
|                     className={`prose dark:prose-invert whitespace-pre-line	 prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${ | ||||
|                       props.message_type && | ||||
|                       "italic text-gray-500 dark:text-gray-400 text-xs" | ||||
|                     }`}>
 | ||||
|                     {props.message} | ||||
|                   </p> | ||||
|                 ) | ||||
|  | ||||
| @ -35,6 +35,7 @@ export const SidePanelBody = () => { | ||||
|           currentMessageIndex={index} | ||||
|           totalMessages={messages.length} | ||||
|           onRengerate={regenerateLastMessage} | ||||
|           message_type={message.messageType} | ||||
|           isProcessing={streaming} | ||||
|           isSearchingInternet={isSearchingInternet} | ||||
|           sources={message.sources} | ||||
|  | ||||
| @ -31,6 +31,7 @@ type Message = { | ||||
|   sources?: string[] | ||||
|   search?: WebSearch | ||||
|   createdAt: number | ||||
|   messageType?: string | ||||
| } | ||||
| 
 | ||||
| type Webshare = { | ||||
| @ -241,7 +242,8 @@ export const saveMessage = async ( | ||||
|   content: string, | ||||
|   images: string[], | ||||
|   source?: any[], | ||||
|   time?: number | ||||
|   time?: number, | ||||
|   message_type?: string | ||||
| ) => { | ||||
|   const id = generateID() | ||||
|   let createdAt = Date.now() | ||||
| @ -256,7 +258,8 @@ export const saveMessage = async ( | ||||
|     content, | ||||
|     images, | ||||
|     createdAt, | ||||
|     sources: source | ||||
|     sources: source, | ||||
|     messageType: message_type | ||||
|   } | ||||
|   const db = new PageAssitDatabase() | ||||
|   await db.addMessage(message) | ||||
|  | ||||
| @ -1,84 +1,10 @@ | ||||
| import { getOllamaURL, isOllamaRunning } from "../services/ollama" | ||||
| import { browser } from "wxt/browser" | ||||
| import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action" | ||||
| 
 | ||||
| const progressHuman = (completed: number, total: number) => { | ||||
|   return ((completed / total) * 100).toFixed(0) + "%" | ||||
| } | ||||
| 
 | ||||
| const clearBadge = () => { | ||||
|   setBadgeText({ text: "" }) | ||||
|   setTitle({ title: "" }) | ||||
| } | ||||
| const streamDownload = async (url: string, model: string) => { | ||||
|   url += "/api/pull" | ||||
|   const response = await fetch(url, { | ||||
|     method: "POST", | ||||
|     headers: { | ||||
|       "Content-Type": "application/json" | ||||
|     }, | ||||
|     body: JSON.stringify({ model, stream: true }) | ||||
|   }) | ||||
| 
 | ||||
|   const reader = response.body?.getReader() | ||||
| 
 | ||||
|   const decoder = new TextDecoder() | ||||
| 
 | ||||
|   let isSuccess = true | ||||
|   while (true) { | ||||
|     if (!reader) { | ||||
|       break | ||||
|     } | ||||
|     const { done, value } = await reader.read() | ||||
| 
 | ||||
|     if (done) { | ||||
|       break | ||||
|     } | ||||
| 
 | ||||
|     const text = decoder.decode(value) | ||||
|     try { | ||||
|       const json = JSON.parse(text.trim()) as { | ||||
|         status: string | ||||
|         total?: number | ||||
|         completed?: number | ||||
|       } | ||||
|       if (json.total && json.completed) { | ||||
|         setBadgeText({ | ||||
|           text: progressHuman(json.completed, json.total) | ||||
|         }) | ||||
|         setBadgeBackgroundColor({ color: "#0000FF" }) | ||||
|       } else { | ||||
|         setBadgeText({ text: "🏋️♂️" }) | ||||
|         setBadgeBackgroundColor({ color: "#FFFFFF" }) | ||||
|       } | ||||
| 
 | ||||
|       setTitle({ title: json.status }) | ||||
| 
 | ||||
|       if (json.status === "success") { | ||||
|         isSuccess = true | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.error(e) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (isSuccess) { | ||||
|     setBadgeText({ text: "✅" }) | ||||
|     setBadgeBackgroundColor({ color: "#00FF00" }) | ||||
|     setTitle({ title: "Model pulled successfully" }) | ||||
|   } else { | ||||
|     setBadgeText({ text: "❌" }) | ||||
|     setBadgeBackgroundColor({ color: "#FF0000" }) | ||||
|     setTitle({ title: "Model pull failed" }) | ||||
|   } | ||||
| 
 | ||||
|   setTimeout(() => { | ||||
|     clearBadge() | ||||
|   }, 5000) | ||||
| } | ||||
| import { clearBadge, streamDownload } from "@/utils/pull-ollama" | ||||
| 
 | ||||
| export default defineBackground({ | ||||
|   main() { | ||||
|     let isCopilotRunning: boolean = false | ||||
|     browser.runtime.onMessage.addListener(async (message) => { | ||||
|       if (message.type === "sidepanel") { | ||||
|         await browser.sidebarAction.open() | ||||
| @ -100,6 +26,15 @@ export default defineBackground({ | ||||
|       } | ||||
|     }) | ||||
| 
 | ||||
|     browser.runtime.onConnect.addListener((port) => { | ||||
|       if (port.name === "pgCopilot") { | ||||
|         isCopilotRunning = true | ||||
|         port.onDisconnect.addListener(() => { | ||||
|           isCopilotRunning = false | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
| 
 | ||||
|     if (import.meta.env.BROWSER === "chrome") { | ||||
|       chrome.action.onClicked.addListener((tab) => { | ||||
|         chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") }) | ||||
| @ -124,10 +59,41 @@ export default defineBackground({ | ||||
|     browser.contextMenus.create({ | ||||
|       id: contextMenuId["sidePanel"], | ||||
|       title: contextMenuTitle["sidePanel"], | ||||
|       contexts: ["all"] | ||||
|       contexts: ["page", "selection"] | ||||
|     }) | ||||
| 
 | ||||
|     browser.contextMenus.create({ | ||||
|       id: "summarize-pa", | ||||
|       title: "Summarize", | ||||
|       contexts: ["selection"] | ||||
|     }) | ||||
| 
 | ||||
|     browser.contextMenus.create({ | ||||
|       id: "explain-pa", | ||||
|       title: "Explain", | ||||
|       contexts: ["selection"] | ||||
|     }) | ||||
| 
 | ||||
|     browser.contextMenus.create({ | ||||
|       id: "rephrase-pa", | ||||
|       title: "Rephrase", | ||||
|       contexts: ["selection"] | ||||
|     }) | ||||
| 
 | ||||
|     browser.contextMenus.create({ | ||||
|       id: "translate-pg", | ||||
|       title: "Translate", | ||||
|       contexts: ["selection"] | ||||
|     }) | ||||
| 
 | ||||
|     // browser.contextMenus.create({
 | ||||
|     //   id: "custom-pg",
 | ||||
|     //   title: "Custom",
 | ||||
|     //   contexts: ["selection"]
 | ||||
|     // })
 | ||||
| 
 | ||||
|     if (import.meta.env.BROWSER === "chrome") { | ||||
|       browser.contextMenus.onClicked.addListener((info, tab) => { | ||||
|       browser.contextMenus.onClicked.addListener(async (info, tab) => { | ||||
|         if (info.menuItemId === "open-side-panel-pa") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
| @ -136,6 +102,68 @@ export default defineBackground({ | ||||
|           browser.tabs.create({ | ||||
|             url: browser.runtime.getURL("/options.html") | ||||
|           }) | ||||
|         } else if (info.menuItemId === "summarize-pa") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
|           }) | ||||
|           // this is a bad method hope somone can fix it :)
 | ||||
|           setTimeout(async () => { | ||||
|             await browser.runtime.sendMessage({ | ||||
|               from: "background", | ||||
|               type: "summary", | ||||
|               text: info.selectionText | ||||
|             }) | ||||
|           }, isCopilotRunning ? 0 : 5000) | ||||
| 
 | ||||
|         } else if (info.menuItemId === "rephrase-pa") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
|           }) | ||||
|           setTimeout(async () => { | ||||
| 
 | ||||
|             await browser.runtime.sendMessage({ | ||||
|               type: "rephrase", | ||||
|               from: "background", | ||||
|               text: info.selectionText | ||||
|             }) | ||||
|           }, isCopilotRunning ? 0 : 5000) | ||||
| 
 | ||||
|         } else if (info.menuItemId === "translate-pg") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
|           }) | ||||
| 
 | ||||
|           setTimeout(async () => { | ||||
|             await browser.runtime.sendMessage({ | ||||
|               type: "translate", | ||||
|               from: "background", | ||||
|               text: info.selectionText | ||||
|             }) | ||||
|           }, isCopilotRunning ? 0 : 5000) | ||||
|         } else if (info.menuItemId === "explain-pa") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
|           }) | ||||
| 
 | ||||
|           setTimeout(async () => { | ||||
|             await browser.runtime.sendMessage({ | ||||
|               type: "explain", | ||||
|               from: "background", | ||||
|               text: info.selectionText | ||||
|             }) | ||||
|           }, isCopilotRunning ? 0 : 5000) | ||||
|         } else if (info.menuItemId === "custom-pg") { | ||||
|           chrome.sidePanel.open({ | ||||
|             tabId: tab.id! | ||||
|           }) | ||||
| 
 | ||||
|           setTimeout(async () => { | ||||
|             await browser.runtime.sendMessage({ | ||||
|               type: "custom", | ||||
|               from: "background", | ||||
|               text: info.selectionText | ||||
|             }) | ||||
|           }, isCopilotRunning ? 0 : 5000) | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|  | ||||
| @ -13,7 +13,8 @@ export const saveMessageOnError = async ({ | ||||
|   selectedModel, | ||||
|   setHistoryId, | ||||
|   isRegenerating, | ||||
|   message_source = "web-ui" | ||||
|   message_source = "web-ui", | ||||
|   message_type  | ||||
| }: { | ||||
|   e: any | ||||
|   setHistory: (history: ChatHistory) => void | ||||
| @ -26,6 +27,7 @@ export const saveMessageOnError = async ({ | ||||
|   setHistoryId: (historyId: string) => void | ||||
|   isRegenerating: boolean | ||||
|   message_source?: "copilot" | "web-ui" | ||||
|   message_type?: string | ||||
| }) => { | ||||
|   if ( | ||||
|     e?.name === "AbortError" || | ||||
| @ -55,7 +57,8 @@ export const saveMessageOnError = async ({ | ||||
|           userMessage, | ||||
|           [image], | ||||
|           [], | ||||
|           1 | ||||
|           1, | ||||
|           message_type | ||||
|         ) | ||||
|       } | ||||
|       await saveMessage( | ||||
| @ -65,7 +68,8 @@ export const saveMessageOnError = async ({ | ||||
|         botMessage, | ||||
|         [], | ||||
|         [], | ||||
|         2 | ||||
|         2, | ||||
|         message_type | ||||
|       ) | ||||
|       await setLastUsedChatModel(historyId, selectedModel) | ||||
|     } else { | ||||
| @ -78,7 +82,8 @@ export const saveMessageOnError = async ({ | ||||
|           userMessage, | ||||
|           [image], | ||||
|           [], | ||||
|           1 | ||||
|           1, | ||||
|           message_type | ||||
|         ) | ||||
|       } | ||||
|       await saveMessage( | ||||
| @ -88,7 +93,8 @@ export const saveMessageOnError = async ({ | ||||
|         botMessage, | ||||
|         [], | ||||
|         [], | ||||
|         2 | ||||
|         2, | ||||
|         message_type | ||||
|       ) | ||||
|       setHistoryId(newHistoryId.id) | ||||
|       await setLastUsedChatModel(newHistoryId.id, selectedModel) | ||||
| @ -109,7 +115,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|   image, | ||||
|   fullText, | ||||
|   source, | ||||
|   message_source = "web-ui" | ||||
|   message_source = "web-ui", | ||||
|   message_type | ||||
| }: { | ||||
|   historyId: string | null | ||||
|   setHistoryId: (historyId: string) => void | ||||
| @ -119,7 +126,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|   image: string | ||||
|   fullText: string | ||||
|   source: any[] | ||||
|   message_source?: "copilot" | "web-ui" | ||||
|   message_source?: "copilot" | "web-ui", | ||||
|   message_type?: string | ||||
| }) => { | ||||
|   if (historyId) { | ||||
|     if (!isRegenerate) { | ||||
| @ -130,7 +138,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|         message, | ||||
|         [image], | ||||
|         [], | ||||
|         1 | ||||
|         1, | ||||
|         message_type | ||||
|       ) | ||||
|     } | ||||
|     await saveMessage( | ||||
| @ -140,7 +149,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|       fullText, | ||||
|       [], | ||||
|       source, | ||||
|       2 | ||||
|       2, | ||||
|       message_type | ||||
|     ) | ||||
|     await setLastUsedChatModel(historyId, selectedModel!) | ||||
|   } else { | ||||
| @ -152,7 +162,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|       message, | ||||
|       [image], | ||||
|       [], | ||||
|       1 | ||||
|       1, | ||||
|       message_type | ||||
|     ) | ||||
|     await saveMessage( | ||||
|       newHistoryId.id, | ||||
| @ -161,7 +172,8 @@ export const saveMessageOnSuccess = async ({ | ||||
|       fullText, | ||||
|       [], | ||||
|       source, | ||||
|       2 | ||||
|       2, | ||||
|       message_type | ||||
|     ) | ||||
|     setHistoryId(newHistoryId.id) | ||||
|     await setLastUsedChatModel(newHistoryId.id, selectedModel!) | ||||
|  | ||||
							
								
								
									
										29
									
								
								src/hooks/useBackgroundMessage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/hooks/useBackgroundMessage.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| import { useState, useEffect } from "react" | ||||
| 
 | ||||
| interface Message { | ||||
|   from: string | ||||
|   type: string | ||||
|   text: string | ||||
| } | ||||
| 
 | ||||
| function useBackgroundMessage() { | ||||
|   const [message, setMessage] = useState<Message | null>(null) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const messageListener = (request: Message) => { | ||||
|       if (request.from === "background") { | ||||
|         setMessage(request) | ||||
|       } | ||||
|     } | ||||
|     browser.runtime.connect({ name: 'pgCopilot' }) | ||||
|     browser.runtime.onMessage.addListener(messageListener) | ||||
| 
 | ||||
|     return () => { | ||||
|       browser.runtime.onMessage.removeListener(messageListener) | ||||
|     } | ||||
|   }, []) | ||||
| 
 | ||||
|   return message | ||||
| } | ||||
| 
 | ||||
| export default useBackgroundMessage | ||||
| @ -31,6 +31,7 @@ import { useStoreChatModelSettings } from "@/store/model" | ||||
| import { getAllDefaultModelSettings } from "@/services/model-settings" | ||||
| import { getSystemPromptForWeb } from "@/web/web" | ||||
| import { pageAssistModel } from "@/models" | ||||
| import { getPrompt } from "@/services/application" | ||||
| 
 | ||||
| export const useMessage = () => { | ||||
|   const { | ||||
| @ -51,8 +52,10 @@ export const useMessage = () => { | ||||
|     isSearchingInternet | ||||
|   } = useStoreMessageOption() | ||||
| 
 | ||||
| 
 | ||||
|   const [chatWithWebsiteEmbedding] = useStorage("chatWithWebsiteEmbedding", true) | ||||
|   const [chatWithWebsiteEmbedding] = useStorage( | ||||
|     "chatWithWebsiteEmbedding", | ||||
|     true | ||||
|   ) | ||||
|   const [maxWebsiteContext] = useStorage("maxWebsiteContext", 4028) | ||||
| 
 | ||||
|   const { | ||||
| @ -857,13 +860,206 @@ export const useMessage = () => { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const presetChatMode = async ( | ||||
|     message: string, | ||||
|     image: string, | ||||
|     isRegenerate: boolean, | ||||
|     messages: Message[], | ||||
|     history: ChatHistory, | ||||
|     signal: AbortSignal, | ||||
|     messageType: string | ||||
|   ) => { | ||||
|     setStreaming(true) | ||||
|     const url = await getOllamaURL() | ||||
|     const userDefaultModelSettings = await getAllDefaultModelSettings() | ||||
| 
 | ||||
|     if (image.length > 0) { | ||||
|       image = `data:image/jpeg;base64,${image.split(",")[1]}` | ||||
|     } | ||||
| 
 | ||||
|     const ollama = await pageAssistModel({ | ||||
|       model: selectedModel!, | ||||
|       baseUrl: cleanUrl(url), | ||||
|       keepAlive: | ||||
|         currentChatModelSettings?.keepAlive ?? | ||||
|         userDefaultModelSettings?.keepAlive, | ||||
|       temperature: | ||||
|         currentChatModelSettings?.temperature ?? | ||||
|         userDefaultModelSettings?.temperature, | ||||
|       topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK, | ||||
|       topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP, | ||||
|       numCtx: | ||||
|         currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx, | ||||
|       seed: currentChatModelSettings?.seed | ||||
|     }) | ||||
| 
 | ||||
|     let newMessage: Message[] = [] | ||||
|     let generateMessageId = generateID() | ||||
| 
 | ||||
|     if (!isRegenerate) { | ||||
|       newMessage = [ | ||||
|         ...messages, | ||||
|         { | ||||
|           isBot: false, | ||||
|           name: "You", | ||||
|           message, | ||||
|           sources: [], | ||||
|           images: [image], | ||||
|           messageType: messageType | ||||
|         }, | ||||
|         { | ||||
|           isBot: true, | ||||
|           name: selectedModel, | ||||
|           message: "▋", | ||||
|           sources: [], | ||||
|           id: generateMessageId | ||||
|         } | ||||
|       ] | ||||
|     } else { | ||||
|       newMessage = [ | ||||
|         ...messages, | ||||
|         { | ||||
|           isBot: true, | ||||
|           name: selectedModel, | ||||
|           message: "▋", | ||||
|           sources: [], | ||||
|           id: generateMessageId | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|     setMessages(newMessage) | ||||
|     let fullText = "" | ||||
|     let contentToSave = "" | ||||
| 
 | ||||
|     try { | ||||
| 
 | ||||
|        | ||||
|       const prompt = await getPrompt(messageType) | ||||
|       let humanMessage = new HumanMessage({ | ||||
|         content: [ | ||||
|           { | ||||
|             text: prompt.replace("{text}", message), | ||||
|             type: "text" | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       if (image.length > 0) { | ||||
|         humanMessage = new HumanMessage({ | ||||
|           content: [ | ||||
|             { | ||||
|               text: prompt.replace("{text}", message), | ||||
|               type: "text" | ||||
|             }, | ||||
|             { | ||||
|               image_url: image, | ||||
|               type: "image_url" | ||||
|             } | ||||
|           ] | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       const chunks = await ollama.stream([humanMessage], { | ||||
|         signal: signal | ||||
|       }) | ||||
|       let count = 0 | ||||
|       for await (const chunk of chunks) { | ||||
|         contentToSave += chunk.content | ||||
|         fullText += chunk.content | ||||
|         if (count === 0) { | ||||
|           setIsProcessing(true) | ||||
|         } | ||||
|         setMessages((prev) => { | ||||
|           return prev.map((message) => { | ||||
|             if (message.id === generateMessageId) { | ||||
|               return { | ||||
|                 ...message, | ||||
|                 message: fullText + "▋" | ||||
|               } | ||||
|             } | ||||
|             return message | ||||
|           }) | ||||
|         }) | ||||
|         count++ | ||||
|       } | ||||
| 
 | ||||
|       setMessages((prev) => { | ||||
|         return prev.map((message) => { | ||||
|           if (message.id === generateMessageId) { | ||||
|             return { | ||||
|               ...message, | ||||
|               message: fullText | ||||
|             } | ||||
|           } | ||||
|           return message | ||||
|         }) | ||||
|       }) | ||||
| 
 | ||||
|       setHistory([ | ||||
|         ...history, | ||||
|         { | ||||
|           role: "user", | ||||
|           content: message, | ||||
|           image, | ||||
|           messageType | ||||
|         }, | ||||
|         { | ||||
|           role: "assistant", | ||||
|           content: fullText | ||||
|         } | ||||
|       ]) | ||||
| 
 | ||||
|       await saveMessageOnSuccess({ | ||||
|         historyId, | ||||
|         setHistoryId, | ||||
|         isRegenerate, | ||||
|         selectedModel: selectedModel, | ||||
|         message, | ||||
|         image, | ||||
|         fullText, | ||||
|         source: [], | ||||
|         message_source: "copilot", | ||||
|         message_type: messageType | ||||
|       }) | ||||
| 
 | ||||
|       setIsProcessing(false) | ||||
|       setStreaming(false) | ||||
|     } catch (e) { | ||||
|       const errorSave = await saveMessageOnError({ | ||||
|         e, | ||||
|         botMessage: fullText, | ||||
|         history, | ||||
|         historyId, | ||||
|         image, | ||||
|         selectedModel, | ||||
|         setHistory, | ||||
|         setHistoryId, | ||||
|         userMessage: message, | ||||
|         isRegenerating: isRegenerate, | ||||
|         message_source: "copilot", | ||||
|         message_type: messageType | ||||
|       }) | ||||
| 
 | ||||
|       if (!errorSave) { | ||||
|         notification.error({ | ||||
|           message: t("error"), | ||||
|           description: e?.message || t("somethingWentWrong") | ||||
|         }) | ||||
|       } | ||||
|       setIsProcessing(false) | ||||
|       setStreaming(false) | ||||
|     } finally { | ||||
|       setAbortController(null) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const onSubmit = async ({ | ||||
|     message, | ||||
|     image, | ||||
|     isRegenerate, | ||||
|     controller, | ||||
|     memory, | ||||
|     messages: chatHistory | ||||
|     messages: chatHistory, | ||||
|     messageType | ||||
|   }: { | ||||
|     message: string | ||||
|     image: string | ||||
| @ -871,6 +1067,7 @@ export const useMessage = () => { | ||||
|     messages?: Message[] | ||||
|     memory?: ChatHistory | ||||
|     controller?: AbortController | ||||
|     messageType?: string | ||||
|   }) => { | ||||
|     let signal: AbortSignal | ||||
|     if (!controller) { | ||||
| @ -882,39 +1079,52 @@ export const useMessage = () => { | ||||
|       signal = controller.signal | ||||
|     } | ||||
| 
 | ||||
|     if (chatMode === "normal") { | ||||
|       if (webSearch) { | ||||
|         await searchChatMode( | ||||
|           message, | ||||
|           image, | ||||
|           isRegenerate || false, | ||||
|           messages, | ||||
|           memory || history, | ||||
|           signal | ||||
|         ) | ||||
|       } else { | ||||
|         await normalChatMode( | ||||
|           message, | ||||
|           image, | ||||
|           isRegenerate, | ||||
|           chatHistory || messages, | ||||
|           memory || history, | ||||
|           signal | ||||
|         ) | ||||
|       } | ||||
|     } else { | ||||
|       const newEmbeddingController = new AbortController() | ||||
|       let embeddingSignal = newEmbeddingController.signal | ||||
|       setEmbeddingController(newEmbeddingController) | ||||
|       await chatWithWebsiteMode( | ||||
|     // this means that the user is trying to send something from a selected text on the web
 | ||||
|     if (messageType) { | ||||
|       await presetChatMode( | ||||
|         message, | ||||
|         image, | ||||
|         isRegenerate, | ||||
|         chatHistory || messages, | ||||
|         memory || history, | ||||
|         signal, | ||||
|         embeddingSignal | ||||
|         messageType | ||||
|       ) | ||||
|     } else { | ||||
|       if (chatMode === "normal") { | ||||
|         if (webSearch) { | ||||
|           await searchChatMode( | ||||
|             message, | ||||
|             image, | ||||
|             isRegenerate || false, | ||||
|             messages, | ||||
|             memory || history, | ||||
|             signal | ||||
|           ) | ||||
|         } else { | ||||
|           await normalChatMode( | ||||
|             message, | ||||
|             image, | ||||
|             isRegenerate, | ||||
|             chatHistory || messages, | ||||
|             memory || history, | ||||
|             signal | ||||
|           ) | ||||
|         } | ||||
|       } else { | ||||
|         const newEmbeddingController = new AbortController() | ||||
|         let embeddingSignal = newEmbeddingController.signal | ||||
|         setEmbeddingController(newEmbeddingController) | ||||
|         await chatWithWebsiteMode( | ||||
|           message, | ||||
|           image, | ||||
|           isRegenerate, | ||||
|           chatHistory || messages, | ||||
|           memory || history, | ||||
|           signal, | ||||
|           embeddingSignal | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -982,7 +1192,8 @@ export const useMessage = () => { | ||||
|           image: lastMessage.image || "", | ||||
|           isRegenerate: true, | ||||
|           memory: newHistory, | ||||
|           controller: newController | ||||
|           controller: newController, | ||||
|           messageType: lastMessage.messageType | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -3,8 +3,11 @@ import { | ||||
|   formatToMessage, | ||||
|   getRecentChatFromCopilot | ||||
| } from "@/db" | ||||
| import useBackgroundMessage from "@/hooks/useBackgroundMessage" | ||||
| import { copilotResumeLastChat } from "@/services/app" | ||||
| import { notification } from "antd" | ||||
| import React from "react" | ||||
| import { useTranslation } from "react-i18next" | ||||
| import { SidePanelBody } from "~/components/Sidepanel/Chat/body" | ||||
| import { SidepanelForm } from "~/components/Sidepanel/Chat/form" | ||||
| import { SidepanelHeader } from "~/components/Sidepanel/Chat/header" | ||||
| @ -13,17 +16,27 @@ import { useMessage } from "~/hooks/useMessage" | ||||
| const SidepanelChat = () => { | ||||
|   const drop = React.useRef<HTMLDivElement>(null) | ||||
|   const [dropedFile, setDropedFile] = React.useState<File | undefined>() | ||||
|   const { t } = useTranslation(["playground"]) | ||||
|   const [dropState, setDropState] = React.useState< | ||||
|     "idle" | "dragging" | "error" | ||||
|   >("idle") | ||||
|   const { chatMode, messages, setHistory, setHistoryId, setMessages } = | ||||
|     useMessage() | ||||
|   const { | ||||
|     chatMode, | ||||
|     streaming, | ||||
|     onSubmit, | ||||
|     messages, | ||||
|     setHistory, | ||||
|     setHistoryId, | ||||
|     setMessages, | ||||
|     selectedModel | ||||
|   } = useMessage() | ||||
| 
 | ||||
|   const bgMsg = useBackgroundMessage() | ||||
| 
 | ||||
|   const setRecentMessagesOnLoad = async () => { | ||||
| 
 | ||||
|     const isEnabled = await copilotResumeLastChat(); | ||||
|     const isEnabled = await copilotResumeLastChat() | ||||
|     if (!isEnabled) { | ||||
|       return; | ||||
|       return | ||||
|     } | ||||
|     if (messages.length === 0) { | ||||
|       const recentChat = await getRecentChatFromCopilot() | ||||
| @ -92,11 +105,26 @@ const SidepanelChat = () => { | ||||
|     } | ||||
|   }, []) | ||||
| 
 | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     setRecentMessagesOnLoad() | ||||
|   }, []) | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     if (bgMsg && !streaming) { | ||||
|       if (selectedModel) { | ||||
|         onSubmit({ | ||||
|           message: bgMsg.text, | ||||
|           messageType: bgMsg.type, | ||||
|           image: "" | ||||
|         }) | ||||
|       } else { | ||||
|         notification.error({ | ||||
|           message: t("formError.noModel") | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   }, [bgMsg]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       ref={drop} | ||||
|  | ||||
							
								
								
									
										162
									
								
								src/services/application.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/services/application.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,162 @@ | ||||
| import { Storage } from "@plasmohq/storage" | ||||
| const storage = new Storage() | ||||
| 
 | ||||
| const DEFAULT_SUMMARY_PROMPT = `Provide a concise summary of the following text, capturing its main ideas and key points:
 | ||||
| 
 | ||||
| Text: | ||||
| --------- | ||||
| {text} | ||||
| --------- | ||||
| 
 | ||||
| Summarize the text in no more than 3-4 sentences. | ||||
| 
 | ||||
| Response:` | ||||
| 
 | ||||
| const DEFAULT_REPHRASE_PROMPT = `Rewrite the following text in a different way, maintaining its original meaning but using alternative vocabulary and sentence structures:
 | ||||
| 
 | ||||
| Text: | ||||
| --------- | ||||
| {text} | ||||
| --------- | ||||
| 
 | ||||
| Ensure that your rephrased version conveys the same information and intent as the original. | ||||
| 
 | ||||
| Response:` | ||||
| 
 | ||||
| const DEFAULT_TRANSLATE_PROMPT = `Translate the following text from its original language into "english"0. Maintain the tone and style of the original text as much as possible:
 | ||||
| 
 | ||||
| Text: | ||||
| --------- | ||||
| {text} | ||||
| --------- | ||||
| 
 | ||||
| Response:` | ||||
| 
 | ||||
| const DEFAULT_EXPLAIN_PROMPT = `Provide a detailed explanation of the following text, breaking down its key concepts, implications, and context:
 | ||||
| 
 | ||||
| Text: | ||||
| --------- | ||||
| {text} | ||||
| --------- | ||||
| 
 | ||||
| Your explanation should: | ||||
| 
 | ||||
| Clarify any complex terms or ideas | ||||
| Provide relevant background information | ||||
| Discuss the significance or implications of the content | ||||
| Address any potential questions a reader might have | ||||
| Use examples or analogies to illustrate points when appropriate | ||||
| 
 | ||||
| Aim for a comprehensive explanation that would help someone with little prior knowledge fully understand the text. | ||||
| 
 | ||||
| Response:` | ||||
| 
 | ||||
| const DEFAULT_CUSTOM_PROMPT = `{text}` | ||||
| 
 | ||||
| export const getSummaryPrompt = async () => { | ||||
|     return (await storage.get("copilotSummaryPrompt")) || DEFAULT_SUMMARY_PROMPT | ||||
| } | ||||
| 
 | ||||
| export const setSummaryPrompt = async (prompt: string) => { | ||||
|     await storage.set("copilotSummaryPrompt", prompt) | ||||
| } | ||||
| 
 | ||||
| export const getRephrasePrompt = async () => { | ||||
|     return (await storage.get("copilotRephrasePrompt")) || DEFAULT_REPHRASE_PROMPT | ||||
| } | ||||
| 
 | ||||
| export const setRephrasePrompt = async (prompt: string) => { | ||||
|     await storage.set("copilotRephrasePrompt", prompt) | ||||
| } | ||||
| 
 | ||||
| export const getTranslatePrompt = async () => { | ||||
|     return ( | ||||
|         (await storage.get("copilotTranslatePrompt")) || DEFAULT_TRANSLATE_PROMPT | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| export const setTranslatePrompt = async (prompt: string) => { | ||||
|     await storage.set("copilotTranslatePrompt", prompt) | ||||
| } | ||||
| 
 | ||||
| export const getExplainPrompt = async () => { | ||||
|     return (await storage.get("copilotExplainPrompt")) || DEFAULT_EXPLAIN_PROMPT | ||||
| } | ||||
| 
 | ||||
| export const setExplainPrompt = async (prompt: string) => { | ||||
|     await storage.set("copilotExplainPrompt", prompt) | ||||
| } | ||||
| 
 | ||||
| export const getCustomPrompt = async () => { | ||||
|     return (await storage.get("copilotCustomPrompt")) || DEFAULT_CUSTOM_PROMPT | ||||
| } | ||||
| 
 | ||||
| export const setCustomPrompt = async (prompt: string) => { | ||||
|     await storage.set("copilotCustomPrompt", prompt) | ||||
| } | ||||
| 
 | ||||
| export const getAllCopilotPrompts = async () => { | ||||
|     const [ | ||||
|         summaryPrompt, | ||||
|         rephrasePrompt, | ||||
|         translatePrompt, | ||||
|         explainPrompt, | ||||
|         customPrompt | ||||
|     ] = await Promise.all([ | ||||
|         getSummaryPrompt(), | ||||
|         getRephrasePrompt(), | ||||
|         getTranslatePrompt(), | ||||
|         getExplainPrompt(), | ||||
|         getCustomPrompt() | ||||
|     ]) | ||||
| 
 | ||||
|     return [ | ||||
|         { key: "summary", prompt: summaryPrompt }, | ||||
|         { key: "rephrase", prompt: rephrasePrompt }, | ||||
|         { key: "translate", prompt: translatePrompt }, | ||||
|         { key: "explain", prompt: explainPrompt }, | ||||
|         { key: "custom", prompt: customPrompt } | ||||
|     ] | ||||
| } | ||||
| 
 | ||||
| export const setAllCopilotPrompts = async ( | ||||
|     prompts: { key: string; prompt: string }[] | ||||
| ) => { | ||||
|     for (const { key, prompt } of prompts) { | ||||
|         switch (key) { | ||||
|             case "summary": | ||||
|                 await setSummaryPrompt(prompt) | ||||
|                 break | ||||
|             case "rephrase": | ||||
|                 await setRephrasePrompt(prompt) | ||||
|                 break | ||||
|             case "translate": | ||||
|                 await setTranslatePrompt(prompt) | ||||
|                 break | ||||
|             case "explain": | ||||
|                 await setExplainPrompt(prompt) | ||||
|                 break | ||||
|             case "custom": | ||||
|                 await setCustomPrompt(prompt) | ||||
|                 break | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export const getPrompt = async (key: string) => { | ||||
|     switch (key) { | ||||
|         case "summary": | ||||
|             return await getSummaryPrompt() | ||||
|         case "rephrase": | ||||
|             return await getRephrasePrompt() | ||||
|         case "translate": | ||||
|             return await getTranslatePrompt() | ||||
|         case "explain": | ||||
|             return await getExplainPrompt() | ||||
|         case "custom": | ||||
|             return await getCustomPrompt() | ||||
|         default: | ||||
|             return "" | ||||
|     } | ||||
| } | ||||
| @ -12,6 +12,7 @@ export type ChatHistory = { | ||||
|   role: "user" | "assistant" | "system" | ||||
|   content: string | ||||
|   image?: string | ||||
|   messageType?: string | ||||
| }[] | ||||
| 
 | ||||
| type State = { | ||||
|  | ||||
| @ -18,12 +18,14 @@ export type Message = { | ||||
|   images?: string[] | ||||
|   search?: WebSearch | ||||
|   id?: string | ||||
|   messageType?: string | ||||
| } | ||||
| 
 | ||||
| export type ChatHistory = { | ||||
|   role: "user" | "assistant" | "system" | ||||
|   content: string | ||||
|   image?: string | ||||
|   image?: string, | ||||
|   messageType?: string | ||||
| }[] | ||||
| 
 | ||||
| type State = { | ||||
|  | ||||
| @ -14,5 +14,6 @@ type WebSearch = { | ||||
|     sources: any[] | ||||
|     images?: string[] | ||||
|     search?: WebSearch | ||||
|     messageType?: string | ||||
|     id?: string | ||||
|   } | ||||
							
								
								
									
										77
									
								
								src/utils/pull-ollama.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/utils/pull-ollama.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | ||||
| import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action" | ||||
| 
 | ||||
| 
 | ||||
| export const progressHuman = (completed: number, total: number) => { | ||||
|     return ((completed / total) * 100).toFixed(0) + "%" | ||||
| } | ||||
| 
 | ||||
| export const clearBadge = () => { | ||||
|     setBadgeText({ text: "" }) | ||||
|     setTitle({ title: "" }) | ||||
| } | ||||
| export const streamDownload = async (url: string, model: string) => { | ||||
|     url += "/api/pull" | ||||
|     const response = await fetch(url, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|             "Content-Type": "application/json" | ||||
|         }, | ||||
|         body: JSON.stringify({ model, stream: true }) | ||||
|     }) | ||||
| 
 | ||||
|     const reader = response.body?.getReader() | ||||
| 
 | ||||
|     const decoder = new TextDecoder() | ||||
| 
 | ||||
|     let isSuccess = true | ||||
|     while (true) { | ||||
|         if (!reader) { | ||||
|             break | ||||
|         } | ||||
|         const { done, value } = await reader.read() | ||||
| 
 | ||||
|         if (done) { | ||||
|             break | ||||
|         } | ||||
| 
 | ||||
|         const text = decoder.decode(value) | ||||
|         try { | ||||
|             const json = JSON.parse(text.trim()) as { | ||||
|                 status: string | ||||
|                 total?: number | ||||
|                 completed?: number | ||||
|             } | ||||
|             if (json.total && json.completed) { | ||||
|                 setBadgeText({ | ||||
|                     text: progressHuman(json.completed, json.total) | ||||
|                 }) | ||||
|                 setBadgeBackgroundColor({ color: "#0000FF" }) | ||||
|             } else { | ||||
|                 setBadgeText({ text: "🏋️♂️" }) | ||||
|                 setBadgeBackgroundColor({ color: "#FFFFFF" }) | ||||
|             } | ||||
| 
 | ||||
|             setTitle({ title: json.status }) | ||||
| 
 | ||||
|             if (json.status === "success") { | ||||
|                 isSuccess = true | ||||
|             } | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (isSuccess) { | ||||
|         setBadgeText({ text: "✅" }) | ||||
|         setBadgeBackgroundColor({ color: "#00FF00" }) | ||||
|         setTitle({ title: "Model pulled successfully" }) | ||||
|     } else { | ||||
|         setBadgeText({ text: "❌" }) | ||||
|         setBadgeBackgroundColor({ color: "#FF0000" }) | ||||
|         setTitle({ title: "Model pull failed" }) | ||||
|     } | ||||
| 
 | ||||
|     setTimeout(() => { | ||||
|         clearBadge() | ||||
|     }, 5000) | ||||
| } | ||||
| @ -50,7 +50,7 @@ export default defineConfig({ | ||||
|   outDir: "build", | ||||
| 
 | ||||
|   manifest: { | ||||
|     version: "1.1.16", | ||||
|     version: "1.2.0", | ||||
|     name: | ||||
|       process.env.TARGET === "firefox" | ||||
|         ? "Page Assist - A Web UI for Local AI Models" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user