mirror of
				https://github.com/open-webui/extension
				synced 2025-06-26 18:25:58 +00:00 
			
		
		
		
	feat: extension config
This commit is contained in:
		
							parent
							
								
									344262154d
								
							
						
					
					
						commit
						b5d41d86f6
					
				| @ -13,6 +13,29 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { | |||||||
|         console.log(res); |         console.log(res); | ||||||
|         sendResponse({ data: res[0]["result"] }); |         sendResponse({ data: res[0]["result"] }); | ||||||
|       }); |       }); | ||||||
|  |   } else if (request.action == "writeText") { | ||||||
|  |     function writeTextToInput(text) { | ||||||
|  |       const activeElement = document.activeElement; | ||||||
|  |       if ( | ||||||
|  |         activeElement && | ||||||
|  |         (activeElement.tagName === "INPUT" || | ||||||
|  |           activeElement.tagName === "TEXTAREA") | ||||||
|  |       ) { | ||||||
|  |         activeElement.value = `${activeElement.value}${text}`; | ||||||
|  | 
 | ||||||
|  |         if (activeElement.tagName === "TEXTAREA") { | ||||||
|  |           activeElement.scrollTop = activeElement.scrollHeight; | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         console.warn("No active input or textarea field found."); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     chrome.scripting.executeScript({ | ||||||
|  |       target: { tabId: id, allFrames: true }, | ||||||
|  |       func: writeTextToInput, | ||||||
|  |       args: [request.text], | ||||||
|  |     }); | ||||||
|  |     sendResponse({}); | ||||||
|   } else { |   } else { | ||||||
|     sendResponse({}); |     sendResponse({}); | ||||||
|   } |   } | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								extension/dist/main.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								extension/dist/main.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								extension/dist/style.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								extension/dist/style.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										89
									
								
								extension/src/apis/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								extension/src/apis/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | export const getOpenAIModels = async (token = "", url = "") => { | ||||||
|  |   let error = null; | ||||||
|  | 
 | ||||||
|  |   const res = await fetch(`${url}/models`, { | ||||||
|  |     method: "GET", | ||||||
|  |     headers: { | ||||||
|  |       Accept: "application/json", | ||||||
|  |       "Content-Type": "application/json", | ||||||
|  |       ...(token && { authorization: `Bearer ${token}` }), | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|  |     .then(async (res) => { | ||||||
|  |       if (!res.ok) throw await res.json(); | ||||||
|  |       return res.json(); | ||||||
|  |     }) | ||||||
|  |     .catch((err) => { | ||||||
|  |       error = `OpenAI: ${err?.error?.message ?? "Network Problem"}`; | ||||||
|  |       return []; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |   if (error) { | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const models = Array.isArray(res) ? res : res?.data ?? null; | ||||||
|  | 
 | ||||||
|  |   return models | ||||||
|  |     ? models | ||||||
|  |         .map((model) => ({ | ||||||
|  |           id: model.id, | ||||||
|  |           name: model.name ?? model.id, | ||||||
|  |           url: url, | ||||||
|  |           custom_info: model.custom_info, | ||||||
|  |         })) | ||||||
|  |         .sort((a, b) => { | ||||||
|  |           return a.name.localeCompare(b.name); | ||||||
|  |         }) | ||||||
|  |     : models; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getModels = async (key, url) => { | ||||||
|  |   let models = await Promise.all([ | ||||||
|  |     getOpenAIModels(key, `${url}/ollama/v1`).catch((error) => { | ||||||
|  |       console.log(error); | ||||||
|  |       return null; | ||||||
|  |     }), | ||||||
|  |     getOpenAIModels(key, `${url}/openai/api`).catch((error) => { | ||||||
|  |       console.log(error); | ||||||
|  |       return null; | ||||||
|  |     }), | ||||||
|  |   ]); | ||||||
|  | 
 | ||||||
|  |   models = models | ||||||
|  |     .filter((models) => models) | ||||||
|  |     .reduce((a, e, i, arr) => a.concat(e), []); | ||||||
|  | 
 | ||||||
|  |   console.log(models); | ||||||
|  | 
 | ||||||
|  |   return models; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const generateOpenAIChatCompletion = async ( | ||||||
|  |   api_key = "", | ||||||
|  |   body = {}, | ||||||
|  |   url = "http://localhost:8080" | ||||||
|  | ) => { | ||||||
|  |   const controller = new AbortController(); | ||||||
|  |   let error = null; | ||||||
|  | 
 | ||||||
|  |   const res = await fetch(`${url}/chat/completions`, { | ||||||
|  |     signal: controller.signal, | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { | ||||||
|  |       Authorization: `Bearer ${api_key}`, | ||||||
|  |       "Content-Type": "application/json", | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify(body), | ||||||
|  |   }).catch((err) => { | ||||||
|  |     console.log(err); | ||||||
|  |     error = err; | ||||||
|  |     return null; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   if (error) { | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return [res, controller]; | ||||||
|  | }; | ||||||
| @ -1,12 +1,40 @@ | |||||||
| import { useState, useEffect } from "react"; | import { useState, useEffect } from "react"; | ||||||
|  | import { generateOpenAIChatCompletion, getModels } from "../apis"; | ||||||
|  | import { splitStream } from "../utils"; | ||||||
| 
 | 
 | ||||||
| export const SpotlightSearch = () => { | export const SpotlightSearch = () => { | ||||||
|   const [open, setOpen] = useState(false); |   const [open, setOpen] = useState(false); | ||||||
|   const [searchValue, setSearchValue] = useState(""); |   const [searchValue, setSearchValue] = useState(""); | ||||||
| 
 | 
 | ||||||
|  |   const [url, setUrl] = useState(localStorage.getItem("url") ?? ""); | ||||||
|  |   const [key, setKey] = useState(localStorage.getItem("key") ?? ""); | ||||||
|  |   const [model, setModel] = useState(localStorage.getItem("model") ?? ""); | ||||||
|  | 
 | ||||||
|  |   const [showConfig, setShowConfig] = useState(url === "" || key === ""); | ||||||
|  |   const [models, setModels] = useState(null); | ||||||
|  | 
 | ||||||
|  |   const resetConfig = () => { | ||||||
|  |     console.log("resetConfig"); | ||||||
|  |     localStorage.setItem("url", ""); | ||||||
|  |     localStorage.setItem("key", ""); | ||||||
|  |     localStorage.setItem("model", ""); | ||||||
|  | 
 | ||||||
|  |     setUrl(""); | ||||||
|  |     setKey(""); | ||||||
|  |     setModel(""); | ||||||
|  |     setShowConfig(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   // Toggle the menu when ⌘Space+Shift is pressed |   // Toggle the menu when ⌘Space+Shift is pressed | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const down = async (e) => { |     const down = async (e) => { | ||||||
|  |       // Reset the configuration when ⌘Shift+Escape is pressed | ||||||
|  |       if (open && e.shiftKey && e.key === "Escape") { | ||||||
|  |         resetConfig(); | ||||||
|  |       } else if (e.key === "Escape") { | ||||||
|  |         setOpen(false); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if ( |       if ( | ||||||
|         e.key === " " && |         e.key === " " && | ||||||
|         (e.metaKey || e.ctrlKey) && |         (e.metaKey || e.ctrlKey) && | ||||||
| @ -15,12 +43,16 @@ export const SpotlightSearch = () => { | |||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|         setOpen((open) => !open); |         setOpen((open) => !open); | ||||||
| 
 | 
 | ||||||
|         const response = await chrome.runtime.sendMessage({ |         try { | ||||||
|           action: "getSelection", |           const response = await chrome.runtime.sendMessage({ | ||||||
|         }); |             action: "getSelection", | ||||||
|  |           }); | ||||||
| 
 | 
 | ||||||
|         if (response?.data ?? false) { |           if (response?.data ?? false) { | ||||||
|           setSearchValue(response.data); |             setSearchValue(response.data); | ||||||
|  |           } | ||||||
|  |         } catch (error) { | ||||||
|  |           console.log("catch", error); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
| @ -34,14 +66,94 @@ export const SpotlightSearch = () => { | |||||||
|         }, 0); |         }, 0); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (e.key === "Escape") { |       if (key !== "" && url !== "") { | ||||||
|         setOpen(false); |         if ( | ||||||
|  |           e.key === "Enter" && | ||||||
|  |           (e.metaKey || e.ctrlKey) && | ||||||
|  |           (e.shiftKey || e.altKey) | ||||||
|  |         ) { | ||||||
|  |           e.preventDefault(); | ||||||
|  | 
 | ||||||
|  |           try { | ||||||
|  |             const response = await chrome.runtime.sendMessage({ | ||||||
|  |               action: "getSelection", | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             if (response?.data ?? false) { | ||||||
|  |               await chrome.runtime.sendMessage({ | ||||||
|  |                 action: "writeText", | ||||||
|  |                 text: "\n", | ||||||
|  |               }); | ||||||
|  | 
 | ||||||
|  |               const [res, controller] = await generateOpenAIChatCompletion( | ||||||
|  |                 key, | ||||||
|  |                 { | ||||||
|  |                   model: model, | ||||||
|  |                   messages: [ | ||||||
|  |                     { | ||||||
|  |                       role: "system", | ||||||
|  |                       content: "You are a helpful assistant.", | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                       role: "user", | ||||||
|  |                       content: response.data, | ||||||
|  |                     }, | ||||||
|  |                   ], | ||||||
|  |                   stream: true, | ||||||
|  |                 }, | ||||||
|  |                 models.find((m) => m.id === model)?.url | ||||||
|  |               ); | ||||||
|  | 
 | ||||||
|  |               if (res && res.ok) { | ||||||
|  |                 const reader = res.body | ||||||
|  |                   .pipeThrough(new TextDecoderStream()) | ||||||
|  |                   .pipeThrough(splitStream("\n")) | ||||||
|  |                   .getReader(); | ||||||
|  | 
 | ||||||
|  |                 while (true) { | ||||||
|  |                   const { value, done } = await reader.read(); | ||||||
|  |                   if (done) { | ||||||
|  |                     break; | ||||||
|  |                   } | ||||||
|  | 
 | ||||||
|  |                   try { | ||||||
|  |                     let lines = value.split("\n"); | ||||||
|  |                     for (const line of lines) { | ||||||
|  |                       if (line !== "") { | ||||||
|  |                         console.log(line); | ||||||
|  |                         if (line === "data: [DONE]") { | ||||||
|  |                           console.log("DONE"); | ||||||
|  |                         } else { | ||||||
|  |                           let data = JSON.parse(line.replace(/^data: /, "")); | ||||||
|  |                           console.log(data); | ||||||
|  | 
 | ||||||
|  |                           if ("request_id" in data) { | ||||||
|  |                             console.log(data.request_id); | ||||||
|  |                           } else { | ||||||
|  |                             await chrome.runtime.sendMessage({ | ||||||
|  |                               action: "writeText", | ||||||
|  |                               text: data.choices[0].delta.content ?? "", | ||||||
|  |                             }); | ||||||
|  |                           } | ||||||
|  |                         } | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   } catch (error) { | ||||||
|  |                     console.log(error); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } catch (error) { | ||||||
|  |             console.log(error); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     document.addEventListener("keydown", down, { capture: true }); |     document.addEventListener("keydown", down); | ||||||
|     return () => document.removeEventListener("keydown", down); |     return () => document.removeEventListener("keydown", down); | ||||||
|   }, []); |   }, [url, key, model, models, open]); | ||||||
| 
 | 
 | ||||||
|   const submitHandler = (e) => { |   const submitHandler = (e) => { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
| @ -49,13 +161,25 @@ export const SpotlightSearch = () => { | |||||||
|     setSearchValue(""); |     setSearchValue(""); | ||||||
| 
 | 
 | ||||||
|     window.open( |     window.open( | ||||||
|       `http://localhost:8080/?q=${encodeURIComponent(searchValue)}`, |       `${url}/?q=${encodeURIComponent(searchValue)}&models=${model}`, | ||||||
|       "_blank" |       "_blank" | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     setOpen(false); |     setOpen(false); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const initHandler = (e) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  | 
 | ||||||
|  |     localStorage.setItem("url", url); | ||||||
|  |     localStorage.setItem("key", key); | ||||||
|  |     localStorage.setItem("model", model); | ||||||
|  | 
 | ||||||
|  |     console.log(localStorage); | ||||||
|  | 
 | ||||||
|  |     setShowConfig(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return open ? ( |   return open ? ( | ||||||
|     <div |     <div | ||||||
|       className="tlwd-fixed tlwd-top-0 tlwd-right-0 tlwd-left-0 tlwd-bottom-0 tlwd-w-full tlwd-min-h-screen tlwd-h-screen tlwd-flex tlwd-justify-center tlwd-z-[9999999999] tlwd-overflow-hidden tlwd-overscroll-contain" |       className="tlwd-fixed tlwd-top-0 tlwd-right-0 tlwd-left-0 tlwd-bottom-0 tlwd-w-full tlwd-min-h-screen tlwd-h-screen tlwd-flex tlwd-justify-center tlwd-z-[9999999999] tlwd-overflow-hidden tlwd-overscroll-contain" | ||||||
| @ -63,50 +187,239 @@ export const SpotlightSearch = () => { | |||||||
|         setOpen(false); |         setOpen(false); | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       <div className=" tlwd-m-auto tlwd-max-w-xl tlwd-w-full tlwd-pb-32"> |       {showConfig ? ( | ||||||
|         <div className="tlwd-w-full tlwd-flex tlwd-flex-col tlwd-justify-between tlwd-py-2.5 tlwd-px-3.5 tlwd-rounded-2xl tlwd-outline tlwd-outline-1 tlwd-outline-gray-850 tlwd-backdrop-blur-3xl tlwd-bg-gray-850/70 shadow-4xl modal-animation"> |         <> | ||||||
|           <form |           <div className=" tlwd-m-auto tlwd-max-w-sm tlwd-w-full tlwd-pb-32"> | ||||||
|             className="tlwd-text-gray-200 tlwd-w-full tlwd-p-0 tlwd-m-0" |             <div className="tlwd-w-full tlwd-flex tlwd-flex-col tlwd-justify-between tlwd-py-2.5 tlwd-px-3.5 tlwd-rounded-2xl tlwd-outline tlwd-outline-1 tlwd-outline-gray-850 tlwd-backdrop-blur-3xl tlwd-bg-gray-850/70 shadow-4xl modal-animation"> | ||||||
|             onSubmit={submitHandler} |               <form | ||||||
|             onMouseDown={(e) => { |                 className="tlwd-text-gray-200 tlwd-w-full tlwd-p-0 tlwd-m-0" | ||||||
|               e.stopPropagation(); |                 onSubmit={initHandler} | ||||||
|             }} |                 onMouseDown={(e) => { | ||||||
|           > |                   e.stopPropagation(); | ||||||
|             <div className="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full"> |                 }} | ||||||
|               <div className=" tlwd-flex tlwd-items-center"> |  | ||||||
|                 <svg |  | ||||||
|                   xmlns="http://www.w3.org/2000/svg" |  | ||||||
|                   fill="none" |  | ||||||
|                   viewBox="0 0 24 24" |  | ||||||
|                   strokeWidth={2.5} |  | ||||||
|                   stroke="currentColor" |  | ||||||
|                   className="tlwd-size-5" |  | ||||||
|                 > |  | ||||||
|                   <path |  | ||||||
|                     strokeLinecap="round" |  | ||||||
|                     strokeLinejoin="round" |  | ||||||
|                     d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" |  | ||||||
|                   /> |  | ||||||
|                 </svg> |  | ||||||
|               </div> |  | ||||||
|               <input |  | ||||||
|                 id="open-webui-search-input" |  | ||||||
|                 placeholder="Search Open WebUI" |  | ||||||
|                 className="tlwd-p-0 tlwd-m-0 tlwd-text-xl tlwd-w-full tlwd-font-medium tlwd-bg-transparent tlwd-border-none placeholder:tlwd-text-gray-500 tlwd-text-neutral-100 tlwd-outline-none" |  | ||||||
|                 value={searchValue} |  | ||||||
|                 onChange={(e) => setSearchValue(e.target.value)} |  | ||||||
|                 autoComplete="off" |                 autoComplete="off" | ||||||
|               /> |               > | ||||||
|             </div> |                 <div className="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full"> | ||||||
|  |                   <div className=" tlwd-flex tlwd-items-center"> | ||||||
|  |                     <svg | ||||||
|  |                       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                       fill="none" | ||||||
|  |                       viewBox="0 0 24 24" | ||||||
|  |                       strokeWidth={2.5} | ||||||
|  |                       stroke="currentColor" | ||||||
|  |                       className="tlwd-size-5" | ||||||
|  |                     > | ||||||
|  |                       <path | ||||||
|  |                         strokeLinecap="round" | ||||||
|  |                         strokeLinejoin="round" | ||||||
|  |                         d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" | ||||||
|  |                       /> | ||||||
|  |                     </svg> | ||||||
|  |                   </div> | ||||||
|  |                   <input | ||||||
|  |                     id="open-webui-url-input" | ||||||
|  |                     placeholder="Open WebUI URL" | ||||||
|  |                     className="tlwd-p-0 tlwd-m-0 tlwd-text-xl tlwd-w-full tlwd-font-medium tlwd-bg-transparent tlwd-border-none placeholder:tlwd-text-gray-500 tlwd-text-neutral-100 tlwd-outline-none" | ||||||
|  |                     value={url} | ||||||
|  |                     onChange={(e) => { | ||||||
|  |                       setUrl(e.target.value); | ||||||
|  |                     }} | ||||||
|  |                     autoComplete="off" | ||||||
|  |                     required | ||||||
|  |                   /> | ||||||
|  |                 </div> | ||||||
|  |                 <div className="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full tlwd-mt-2"> | ||||||
|  |                   <div className=" tlwd-flex tlwd-items-center"> | ||||||
|  |                     <svg | ||||||
|  |                       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                       fill="none" | ||||||
|  |                       viewBox="0 0 24 24" | ||||||
|  |                       strokeWidth={2.5} | ||||||
|  |                       stroke="currentColor" | ||||||
|  |                       className="tlwd-size-5" | ||||||
|  |                     > | ||||||
|  |                       <path | ||||||
|  |                         strokeLinecap="round" | ||||||
|  |                         strokeLinejoin="round" | ||||||
|  |                         d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" | ||||||
|  |                       /> | ||||||
|  |                     </svg> | ||||||
|  |                   </div> | ||||||
|  |                   <input | ||||||
|  |                     id="open-webui-key-input" | ||||||
|  |                     placeholder="Open WebUI API Key" | ||||||
|  |                     type="password" | ||||||
|  |                     className="tlwd-p-0 tlwd-m-0 tlwd-text-xl tlwd-w-full tlwd-font-medium tlwd-bg-transparent tlwd-border-none placeholder:tlwd-text-gray-500 tlwd-text-neutral-100 tlwd-outline-none" | ||||||
|  |                     value={key} | ||||||
|  |                     onChange={(e) => setKey(e.target.value)} | ||||||
|  |                     autoComplete="off" | ||||||
|  |                     required | ||||||
|  |                   /> | ||||||
|  |                   <button | ||||||
|  |                     className=" tlwd-flex tlwd-items-center tlwd-bg-transparent tlwd-text-neutral-100 tlwd-cursor-pointer tlwd-p-0 tlwd-m-0 tlwd-outline-none tlwd-border-none" | ||||||
|  |                     type="button" | ||||||
|  |                     onClick={async () => { | ||||||
|  |                       let _url = url; | ||||||
| 
 | 
 | ||||||
|             <p className="tlwd-text-right tlwd-text-[0.7rem] tlwd-p-0 tlwd-m-0 tlwd-text-neutral-300"> |                       if (_url.endsWith("/")) { | ||||||
|               Press ⌘Space+Shift to toggle |                         _url = _url.slice(0, -1); | ||||||
|             </p> |                         setUrl(_url); | ||||||
|           </form> |                       } | ||||||
|  | 
 | ||||||
|  |                       setModels(await getModels(key, _url)); | ||||||
|  |                     }} | ||||||
|  |                   > | ||||||
|  |                     <svg | ||||||
|  |                       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                       fill="none" | ||||||
|  |                       viewBox="0 0 24 24" | ||||||
|  |                       strokeWidth={2.5} | ||||||
|  |                       stroke="currentColor" | ||||||
|  |                       className="tlwd-size-5" | ||||||
|  |                     > | ||||||
|  |                       <path | ||||||
|  |                         strokeLinecap="round" | ||||||
|  |                         strokeLinejoin="round" | ||||||
|  |                         d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" | ||||||
|  |                       /> | ||||||
|  |                     </svg> | ||||||
|  |                   </button> | ||||||
|  |                 </div> | ||||||
|  | 
 | ||||||
|  |                 {models && ( | ||||||
|  |                   <div className="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full tlwd-mt-2"> | ||||||
|  |                     <div className=" tlwd-flex tlwd-items-center"> | ||||||
|  |                       <svg | ||||||
|  |                         xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                         fill="none" | ||||||
|  |                         viewBox="0 0 24 24" | ||||||
|  |                         strokeWidth={2.5} | ||||||
|  |                         stroke="currentColor" | ||||||
|  |                         className="tlwd-size-5" | ||||||
|  |                       > | ||||||
|  |                         <path | ||||||
|  |                           strokeLinecap="round" | ||||||
|  |                           strokeLinejoin="round" | ||||||
|  |                           d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" | ||||||
|  |                         /> | ||||||
|  |                       </svg> | ||||||
|  |                     </div> | ||||||
|  |                     <select | ||||||
|  |                       id="open-webui-model-input" | ||||||
|  |                       className="tlwd-p-0 tlwd-m-0 tlwd-text-xl tlwd-w-full tlwd-font-medium tlwd-bg-transparent tlwd-border-none placeholder:tlwd-text-gray-500 tlwd-text-neutral-100 tlwd-outline-none" | ||||||
|  |                       value={model} | ||||||
|  |                       onChange={(e) => setModel(e.target.value)} | ||||||
|  |                       autoComplete="off" | ||||||
|  |                       required | ||||||
|  |                     > | ||||||
|  |                       <option value="">Select a model</option> | ||||||
|  |                       {models.map((model) => ( | ||||||
|  |                         <option key={model.id} value={model.id}> | ||||||
|  |                           {model.name ?? model.id} | ||||||
|  |                         </option> | ||||||
|  |                       ))} | ||||||
|  |                     </select> | ||||||
|  |                     <button | ||||||
|  |                       className=" tlwd-flex tlwd-items-center tlwd-bg-transparent tlwd-text-neutral-100 tlwd-cursor-pointer tlwd-p-0 tlwd-m-0 tlwd-outline-none tlwd-border-none" | ||||||
|  |                       type="submit" | ||||||
|  |                     > | ||||||
|  |                       <svg | ||||||
|  |                         xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                         fill="none" | ||||||
|  |                         viewBox="0 0 24 24" | ||||||
|  |                         strokeWidth={2.5} | ||||||
|  |                         stroke="currentColor" | ||||||
|  |                         className="tlwd-size-5" | ||||||
|  |                       > | ||||||
|  |                         <path | ||||||
|  |                           strokeLinecap="round" | ||||||
|  |                           strokeLinejoin="round" | ||||||
|  |                           d="m4.5 12.75 6 6 9-13.5" | ||||||
|  |                         /> | ||||||
|  |                       </svg> | ||||||
|  |                     </button> | ||||||
|  |                   </div> | ||||||
|  |                 )} | ||||||
|  |               </form> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </> | ||||||
|  |       ) : ( | ||||||
|  |         <div className=" tlwd-m-auto tlwd-max-w-xl tlwd-w-full tlwd-pb-32"> | ||||||
|  |           <div className="tlwd-w-full tlwd-flex tlwd-flex-col tlwd-justify-between tlwd-py-2.5 tlwd-px-3.5 tlwd-rounded-2xl tlwd-outline tlwd-outline-1 tlwd-outline-gray-850 tlwd-backdrop-blur-3xl tlwd-bg-gray-850/70 shadow-4xl modal-animation"> | ||||||
|  |             <form | ||||||
|  |               className="tlwd-text-gray-200 tlwd-w-full tlwd-p-0 tlwd-m-0" | ||||||
|  |               onSubmit={submitHandler} | ||||||
|  |               onMouseDown={(e) => { | ||||||
|  |                 e.stopPropagation(); | ||||||
|  |               }} | ||||||
|  |               autoComplete="off" | ||||||
|  |             > | ||||||
|  |               <div className="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full"> | ||||||
|  |                 <div className=" tlwd-flex tlwd-items-center"> | ||||||
|  |                   <svg | ||||||
|  |                     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                     fill="none" | ||||||
|  |                     viewBox="0 0 24 24" | ||||||
|  |                     strokeWidth={2.5} | ||||||
|  |                     stroke="currentColor" | ||||||
|  |                     className="tlwd-size-5" | ||||||
|  |                   > | ||||||
|  |                     <path | ||||||
|  |                       strokeLinecap="round" | ||||||
|  |                       strokeLinejoin="round" | ||||||
|  |                       d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" | ||||||
|  |                     /> | ||||||
|  |                   </svg> | ||||||
|  |                 </div> | ||||||
|  |                 <input | ||||||
|  |                   id="open-webui-search-input" | ||||||
|  |                   placeholder="Search Open WebUI" | ||||||
|  |                   className="tlwd-p-0 tlwd-m-0 tlwd-text-xl tlwd-w-full tlwd-font-medium tlwd-bg-transparent tlwd-border-none placeholder:tlwd-text-gray-500 tlwd-text-neutral-100 tlwd-outline-none" | ||||||
|  |                   value={searchValue} | ||||||
|  |                   onChange={(e) => setSearchValue(e.target.value)} | ||||||
|  |                   autoComplete="off" | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  | 
 | ||||||
|  |               <div className=" tlwd-flex tlwd-justify-end tlwd-gap-1"> | ||||||
|  |                 <p className="tlwd-text-right tlwd-text-[0.7rem] tlwd-p-0 tlwd-m-0 tlwd-text-neutral-300"> | ||||||
|  |                   Press ⌘Space+Shift to toggle | ||||||
|  |                 </p> | ||||||
|  |                 <button | ||||||
|  |                   className=" tlwd-flex tlwd-items-center tlwd-bg-transparent tlwd-text-neutral-100 tlwd-cursor-pointer tlwd-p-0 tlwd-m-0 tlwd-outline-none tlwd-border-none" | ||||||
|  |                   type="button" | ||||||
|  |                   onClick={() => { | ||||||
|  |                     setShowConfig(true); | ||||||
|  |                   }} | ||||||
|  |                 > | ||||||
|  |                   <svg | ||||||
|  |                     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                     fill="none" | ||||||
|  |                     viewBox="0 0 24 24" | ||||||
|  |                     strokeWidth={2.5} | ||||||
|  |                     stroke="currentColor" | ||||||
|  |                     className="tlwd-size-3" | ||||||
|  |                   > | ||||||
|  |                     <path | ||||||
|  |                       strokeLinecap="round" | ||||||
|  |                       strokeLinejoin="round" | ||||||
|  |                       d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" | ||||||
|  |                     /> | ||||||
|  |                     <path | ||||||
|  |                       strokeLinecap="round" | ||||||
|  |                       strokeLinejoin="round" | ||||||
|  |                       d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" | ||||||
|  |                     /> | ||||||
|  |                   </svg> | ||||||
|  |                 </button> | ||||||
|  |               </div> | ||||||
|  |             </form> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       )} | ||||||
|     </div> |     </div> | ||||||
|   ) : ( |   ) : ( | ||||||
|     <> </> |     <></> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								extension/src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								extension/src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | export const splitStream = (splitOn) => { | ||||||
|  |   let buffer = ""; | ||||||
|  |   return new TransformStream({ | ||||||
|  |     transform(chunk, controller) { | ||||||
|  |       buffer += chunk; | ||||||
|  |       const parts = buffer.split(splitOn); | ||||||
|  |       parts.slice(0, -1).forEach((part) => controller.enqueue(part)); | ||||||
|  |       buffer = parts[parts.length - 1]; | ||||||
|  |     }, | ||||||
|  |     flush(controller) { | ||||||
|  |       if (buffer) controller.enqueue(buffer); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user