refac: migrate to svelte

This commit is contained in:
Timothy J. Baek 2024-05-23 12:51:19 -07:00
parent e26ea0f51b
commit b1c2547f17
24 changed files with 1044 additions and 3492 deletions

View File

@ -1,21 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@ -8,10 +8,14 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
_old
# Editor directories and files
.vscode
.vscode/*
!.vscode/extensions.json
.idea

47
extension/README.md Normal file
View File

@ -0,0 +1,47 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Open WebUI Extension</title>
<script type="module" crossorigin src="/main.js"></script>

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Open WebUI Extension</title>
</head>
<body>
<div id="extension-app"></div>
<script type="module" src="./src/main.jsx"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -6,24 +6,19 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tailwindcss": "^3.4.3",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^5.2.0"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,12 +0,0 @@
import { SpotlightSearch } from "./components/SpotlightSearch";
function App() {
return (
<>
<div className="extension-container">
<SpotlightSearch />
</div>
</>
);
}
export default App;

7
extension/src/App.svelte Normal file
View File

@ -0,0 +1,7 @@
<script lang="ts">
import SpotlightSearch from "./lib/components/SpotlightSearch.svelte";
</script>
<div class="extension-container">
<SpotlightSearch />
</div>

View File

@ -1,467 +0,0 @@
import { useState, useEffect } from "react";
import { generateOpenAIChatCompletion, getModels } from "../apis";
import { splitStream } from "../utils";
export const SpotlightSearch = () => {
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
const [storageCache, setStorageCache] = useState(null);
const [url, setUrl] = useState("");
const [key, setKey] = useState("");
const [model, setModel] = useState("");
const [showConfig, setShowConfig] = useState(true);
const [models, setModels] = useState(null);
useEffect(() => {
async function getStorageCache() {
let _storageCache = null;
try {
_storageCache = await chrome.storage.local.get();
} catch (error) {
console.log(error);
}
setStorageCache(_storageCache);
if (_storageCache) {
setUrl(_storageCache.url ?? "");
setKey(_storageCache.key ?? "");
setModel(_storageCache.model ?? "");
if (_storageCache.url && _storageCache.key && _storageCache.model) {
setModels(await getModels(_storageCache.key, _storageCache.url));
setShowConfig(false);
}
}
}
getStorageCache();
}, []);
const resetConfig = () => {
console.log("resetConfig");
try {
chrome.storage.local.clear().then(() => {
console.log("Value is cleared");
});
} catch (error) {
console.log(error);
localStorage.setItem("url", "");
localStorage.setItem("key", "");
localStorage.setItem("model", "");
}
setUrl("");
setKey("");
setModel("");
setShowConfig(true);
};
// Toggle the menu when Space+Shift is pressed
useEffect(() => {
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 (
e.key === " " &&
(e.metaKey || e.ctrlKey) &&
(e.shiftKey || e.altKey)
) {
e.preventDefault();
setOpen((open) => !open);
try {
const response = await chrome.runtime.sendMessage({
action: "getSelection",
});
if (response?.data ?? false) {
setSearchValue(response.data);
}
} catch (error) {
console.log("catch", error);
}
setTimeout(() => {
const inputElement = document.getElementById(
"open-webui-search-input"
);
if (inputElement) {
inputElement.focus();
}
}, 0);
}
if (key !== "" && url !== "") {
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);
return () => document.removeEventListener("keydown", down);
}, [url, key, model, models, open]);
const submitHandler = (e) => {
e.preventDefault();
console.log(searchValue);
setSearchValue("");
window.open(
`${url}/?q=${encodeURIComponent(searchValue)}&models=${model}`,
"_blank"
);
setOpen(false);
};
const initHandler = (e) => {
e.preventDefault();
try {
chrome.storage.local
.set({ url: url, key: key, model: model })
.then(() => {
console.log("Value is set");
});
} catch (error) {
console.log(error);
localStorage.setItem("url", url);
localStorage.setItem("key", key);
localStorage.setItem("model", model);
}
setShowConfig(false);
};
return open ? (
<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"
onMouseDown={() => {
setOpen(false);
}}
>
{showConfig ? (
<>
<div className=" tlwd-m-auto tlwd-max-w-sm 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={initHandler}
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="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="one-time-code"
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
placeholder="Open WebUI API Key"
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="one-time-code"
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;
if (_url.endsWith("/")) {
_url = _url.slice(0, -1);
setUrl(_url);
}
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 && models.length > 0 && (
<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="one-time-code"
/>
</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>
) : (
<></>
);
};

View File

@ -0,0 +1,456 @@
<script lang="ts">
import { onMount } from "svelte";
import { generateOpenAIChatCompletion, getModels } from "../apis";
import { splitStream } from "../utils";
let show = false;
let showConfig = false;
let url = "";
let key = "";
let model = "";
let searchValue = "";
let models = [];
const resetConfig = () => {
console.log("resetConfig");
try {
chrome.storage.local.clear().then(() => {
console.log("Value is cleared");
});
} catch (error) {
console.log(error);
localStorage.setItem("url", "");
localStorage.setItem("key", "");
localStorage.setItem("model", "");
}
url = "";
key = "";
model = "";
showConfig = true;
};
const submitHandler = (e) => {
e.preventDefault();
window.open(
`${url}/?q=${encodeURIComponent(searchValue)}&models=${model}`,
"_blank"
);
searchValue = "";
show = false;
};
const initHandler = (e) => {
e.preventDefault();
try {
chrome.storage.local
.set({ url: url, key: key, model: model })
.then(() => {
console.log("Value is set");
});
} catch (error) {
console.log(error);
localStorage.setItem("url", url);
localStorage.setItem("key", key);
localStorage.setItem("model", model);
}
showConfig = false;
};
onMount(async () => {
let _storageCache = null;
try {
_storageCache = await chrome.storage.local.get();
} catch (error) {
console.log(error);
}
if (_storageCache) {
url = _storageCache.url ?? "";
key = _storageCache.key ?? "";
model = _storageCache.model ?? "";
if (_storageCache.url && _storageCache.key && _storageCache.model) {
models = await getModels(_storageCache.key, _storageCache.url);
showConfig = false;
}
}
const down = async (e) => {
// Reset the configuration when ⌘Shift+Escape is pressed
if (show && e.shiftKey && e.key === "Escape") {
resetConfig();
} else if (e.key === "Escape") {
show = false;
}
if (
e.key === " " &&
(e.metaKey || e.ctrlKey) &&
(e.shiftKey || e.altKey)
) {
e.preventDefault();
try {
const response = await chrome.runtime.sendMessage({
action: "getSelection",
});
if (response?.data ?? false) {
searchValue = response.data;
}
} catch (error) {
console.log("catch", error);
}
show = !show;
setTimeout(() => {
const inputElement = document.getElementById(
"open-webui-search-input"
);
if (inputElement) {
inputElement.focus();
}
}, 0);
}
if (key !== "" && url !== "") {
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);
return () => document.removeEventListener("keydown", down);
});
</script>
{#if show}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="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"
on:mousedown={() => {
show = false;
}}
>
{#if showConfig}
<div class=" tlwd-m-auto tlwd-max-w-sm tlwd-w-full tlwd-pb-32">
<div
class="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"
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<form
class="tlwd-text-gray-200 tlwd-w-full tlwd-p-0 tlwd-m-0"
on:submit={initHandler}
on:mousedown={(e) => {
e.stopPropagation();
}}
autocomplete="off"
>
<div class="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full">
<div class=" tlwd-flex tlwd-items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="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"
class="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"
bind:value={url}
autocomplete="one-time-code"
required
/>
</div>
<div
class="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full tlwd-mt-2"
>
<div class=" tlwd-flex tlwd-items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="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
placeholder="Open WebUI API Key"
class="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"
bind:value={key}
autocomplete="one-time-code"
required
/>
<button
class=" 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"
on:click={async () => {
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
models = await getModels(key, url);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="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>
{#if models && models.length > 0}
<div
class="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full tlwd-mt-2"
>
<div class=" tlwd-flex tlwd-items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="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"
class="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"
bind:value={model}
autocomplete="off"
required
>
<option value="">Select a model</option>
{#each models as model}
<option value={model.id}>{model.name ?? model.id}</option>
{/each}
</select>
<button
class=" 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"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 12.75 6 6 9-13.5"
/>
</svg>
</button>
</div>
{/if}
</form>
</div>
</div>
{:else}
<div class=" tlwd-m-auto tlwd-max-w-xl tlwd-w-full tlwd-pb-32">
<div
class="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"
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<form
class="tlwd-text-gray-200 tlwd-w-full tlwd-p-0 tlwd-m-0"
on:submit={submitHandler}
on:mousedown={(e) => {
e.stopPropagation();
}}
autocomplete="off"
>
<div class="tlwd-flex tlwd-items-center tlwd-gap-2 tlwd-w-full">
<div class=" tlwd-flex tlwd-items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="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"
class="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"
bind:value={searchValue}
autocomplete="one-time-code"
/>
</div>
<div class=" tlwd-flex tlwd-justify-end tlwd-gap-1">
<p
class="tlwd-text-right tlwd-text-[0.7rem] tlwd-p-0 tlwd-m-0 tlwd-text-neutral-300"
>
Press ⌘Space+Shift to toggle
</p>
<button
class=" 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"
on:click={() => {
showConfig = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={2.5}
stroke="currentColor"
class="tlwd-size-3"
>
<path
stroke-linecap="round"
stroke-linejoin="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
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</div>
</form>
</div>
</div>
{/if}
</div>
{/if}

View File

@ -1,10 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('extension-app')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

8
extension/src/main.ts Normal file
View File

@ -0,0 +1,8 @@
import "./app.css";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("extension-app")!,
});
export default app;

2
extension/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {
colors: {

20
extension/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,9 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { svelte } from "@sveltejs/vite-plugin-svelte";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [svelte()],
build: {
rollupOptions: {
output: {