diff --git a/README.md b/README.md index 87c92e44..3135e219 100644 --- a/README.md +++ b/README.md @@ -17,5 +17,6 @@ A collection of [🤗 Transformers.js](https://huggingface.co/docs/transformers. | [Node.js (CJS)](./node-cjs/) | Sentiment analysis in Node.js w/ CommonJS | n/a | | [Next.js](./next-server/) | Sentiment analysis in Next.js | [Demo](https://huggingface.co/spaces/webml-community/next-server-template) | | [SvelteKit](./sveltekit/) | Sentiment analysis in SvelteKit | [Demo](https://huggingface.co/spaces/webml-community/sveltekit-server-template) | +| [Chrome Extension with Plasmo](https://github.com/tantara/transformers.js-chrome) | Chrome extension with Plasmo | [Demo](https://chromewebstore.google.com/detail/private-ai-assistant-runn/jojlpeliekadmokfnikappfadbjiaghp) | Check out the Transformers.js [template](https://huggingface.co/new-space?template=static-templates%2Ftransformers.js) on Hugging Face to get started in one click! diff --git a/chrome-extension-plasmo/.env b/chrome-extension-plasmo/.env new file mode 100644 index 00000000..6c176842 --- /dev/null +++ b/chrome-extension-plasmo/.env @@ -0,0 +1 @@ +POST_BUILD_SCRIPT=postbuild/sed.js \ No newline at end of file diff --git a/chrome-extension-plasmo/.env.example b/chrome-extension-plasmo/.env.example new file mode 100644 index 00000000..6c176842 --- /dev/null +++ b/chrome-extension-plasmo/.env.example @@ -0,0 +1 @@ +POST_BUILD_SCRIPT=postbuild/sed.js \ No newline at end of file diff --git a/chrome-extension-plasmo/.gitignore b/chrome-extension-plasmo/.gitignore new file mode 100644 index 00000000..590de907 --- /dev/null +++ b/chrome-extension-plasmo/.gitignore @@ -0,0 +1,35 @@ + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +out/ +build/ +dist/ + +# plasmo +.plasmo + +# typescript +.tsbuildinfo + +.vscode \ No newline at end of file diff --git a/chrome-extension-plasmo/.prettierrc.mjs b/chrome-extension-plasmo/.prettierrc.mjs new file mode 100644 index 00000000..77f84c21 --- /dev/null +++ b/chrome-extension-plasmo/.prettierrc.mjs @@ -0,0 +1,26 @@ +/** + * @type {import('prettier').Options} + */ +export default { + printWidth: 80, + tabWidth: 2, + useTabs: false, + semi: false, + singleQuote: false, + trailingComma: "none", + bracketSpacing: true, + bracketSameLine: true, + plugins: ["@ianvs/prettier-plugin-sort-imports"], + importOrder: [ + "", // Node.js built-in modules + "", // Imports not matched by other special words or groups. + "", // Empty line + "^@plasmo/(.*)$", + "", + "^@plasmohq/(.*)$", + "", + "^~(.*)$", + "", + "^[./]" + ] +} diff --git a/chrome-extension-plasmo/LICENSE b/chrome-extension-plasmo/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/chrome-extension-plasmo/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/chrome-extension-plasmo/README.md b/chrome-extension-plasmo/README.md new file mode 100644 index 00000000..d78595a2 --- /dev/null +++ b/chrome-extension-plasmo/README.md @@ -0,0 +1,114 @@ +# Transformers.js Chrome Extension + +This is an example Chrome extension for [Transformers.js](https://github.com/huggingface/transformers.js), a library for running LLMs in the browser, built on top of [Plasmo](https://plasmo.com/). + +Please note that this project is still under development. The chrome extension process could be stopped by the browser anytime. Please refer to the [tantara/transformers.js-chrome](https://github.com/tantara/transformers.js-chrome) repo for the latest updates. + +## Examples + +Here is the link to the [demo video](https://www.youtube.com/watch?v=yXZQ8FHtSes). Each example will be updated below. + +| Task | Example | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| Text Summarization | ![Example Text Summarization](./docs/example-summarize.jpg) | +| Code Generation | ![Example Code Generation](./docs/example-write-code.jpg) | +| Multi Modal LLM | [https://github.com/tantara/transformers.js-chrome](https://github.com/tantara/transformers.js-chrome) | +| Speech to Text | [https://github.com/tantara/transformers.js-chrome](https://github.com/tantara/transformers.js-chrome) | +| Reasoning | [https://github.com/tantara/transformers.js-chrome](https://github.com/tantara/transformers.js-chrome) | +| Image Generation | [https://github.com/tantara/transformers.js-chrome](https://github.com/tantara/transformers.js-chrome) | + +## Features + +- [x] Integrate Transformers.js with Chrome extension +- [x] Use modern web development tooling (TypeScript, Parcel, Tailwind CSS, Shadcn, etc.) +- [x] Change generation parameters (e.g. max_tokens, temperature, top_p etc.) +- [x] Load LLaMA variants +- [x] Load other LLM models +- [x] Release extension to Chrome Web Store + +## Performance + +All the numbers below are measured on a MacBook Pro M1 Max with 32GB RAM. + +Prompt: "Write python code to compute the nth fibonacci number." + +| Model | Throughput | +| ----------------------------------------------------------------------------------------------- | --------------- | +| [Llama-3.2-1B](https://huggingface.co/onnx-community/Llama-3.2-1B-Instruct-q4f16) (q4f16) | 40.3 tokens/sec | +| [Phi-3.5-mini](https://huggingface.co/onnx-community/Phi-3.5-mini-instruct-onnx-web) (q4f16) | 32.9 tokens/sec | +| [SmolLM2-1.7B](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct) (q4f16) | 46.2 tokens/sec | +| [Qwen2.5-Coder-1.5B](https://huggingface.co/onnx-community/Qwen2.5-Coder-1.5B-Instruct) (q4f16) | 36.1 tokens/sec | + + +## Installation + +### Chrome Web Store + +Install '[Private AI Assistant[(https://chromewebstore.google.com/detail/private-ai-assistant-runn/jojlpeliekadmokfnikappfadbjiaghp)]' from the Chrome Web Store. + +### From source + +You should install `node` and `pnpm` to build the project. + +First, install the dependencies: + +```bash +pnpm install +``` + +Then, start the development server: + +```bash +pnpm dev +``` + +Open your Chrome browser (i.e. `chrome://extensions`) and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`. + +For further guidance, [visit Plasmo's Documentation](https://docs.plasmo.com/) or create an issue. + +## Deployment + +### Making production build + +Run the following: + +```bash +pnpm build +``` + +This should create a production bundle for your extension, ready to be zipped and published to the stores. + +### Submit to the webstores + +The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission! + +## Debugging + +### Debug service worker + +Open `chrome://extensions` and find the "Inspect views" section for the extension. + +![Inspect views](./docs/inspect-views.jpg) + +### Memory usage for inference + +Open Chrome > More Tools > Task Manager. + +![Task manager](./docs/task-manager.jpg) + +### Local storage for cached checkpoints + +Run Chrome extension, open `inspect`, go to `Application` tab, find `Local Storage` section, and find the `transformers-cache` entry. + +![Local storage](./docs/local-storage.jpg) + +## References + +- [Transformers.js Example](https://github.com/huggingface/transformers.js-examples) +- [Transformers.js V2 Chrome Extension](https://github.com/huggingface/transformers.js/tree/main/examples/extension) +- [Plasmo Documentation](https://docs.plasmo.com/) +- [WebLLM](https://webllm.mlc.ai/) and its [Chrome Extension](https://github.com/mlc-ai/web-llm/tree/main/examples/chrome-extension-webgpu-service-worker) +- [gpu.cpp](https://github.com/AnswerDotAI/gpu.cpp) +- https://github.com/huggingface/transformers.js/issues/986 +- https://github.com/microsoft/onnxruntime/issues/20876 +- https://github.com/ggaabe/extension diff --git a/chrome-extension-plasmo/assets/icon.png b/chrome-extension-plasmo/assets/icon.png new file mode 100644 index 00000000..2e4f723d Binary files /dev/null and b/chrome-extension-plasmo/assets/icon.png differ diff --git a/chrome-extension-plasmo/components.json b/chrome-extension-plasmo/components.json new file mode 100644 index 00000000..61fcdd93 --- /dev/null +++ b/chrome-extension-plasmo/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/chrome-extension-plasmo/components/ChangeModelForm.tsx b/chrome-extension-plasmo/components/ChangeModelForm.tsx new file mode 100644 index 00000000..4a0c7795 --- /dev/null +++ b/chrome-extension-plasmo/components/ChangeModelForm.tsx @@ -0,0 +1,76 @@ +import { useStorage } from "@plasmohq/storage/hook" + +import { DEFAULT_MODEL_CONFIG } from "~/llm/default-config" +import { modelList } from "~/llm/model-list" +import type { ModelConfig } from "~/src/types" + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from "./ui/accordion" +import { Alert, AlertDescription } from "./ui/alert" +import { Button } from "./ui/button" + +function ChangeModelForm() { + const [config, setConfig] = useStorage( + "model_config", + DEFAULT_MODEL_CONFIG + ) + const availableModels = modelList + + const handleChange = async (model: ModelConfig) => { + await setConfig(model) + alert("Model changed. Please refresh the page to apply the changes.") + chrome.runtime.reload() + } + + return ( +
+ + {availableModels.map((model) => ( + + {model.model_id} + + {config.model_id == model.model_id && ( + + + You're using this model {model.model_id}. + + + )} +
+ {JSON.stringify(model, null, 2)} +
+
+ + +
+
+
+ ))} +
+
+ ) +} + +export default ChangeModelForm diff --git a/chrome-extension-plasmo/components/Chat.tsx b/chrome-extension-plasmo/components/Chat.tsx new file mode 100644 index 00000000..000a17eb --- /dev/null +++ b/chrome-extension-plasmo/components/Chat.tsx @@ -0,0 +1,226 @@ +import { ArrowUp } from "lucide-react" +import { useCallback, useEffect, useRef, useState } from "react" + +import { useStorage } from "@plasmohq/storage/hook" + +import ChatExamples from "~/components/ChatExamples" +import ChatHeader from "~/components/ChatHeader" +import ChatMessages from "~/components/ChatMessages" +import ChatProgress from "~/components/ChatProgress" +import { Button } from "~/components/ui/button" +import { Input } from "~/components/ui/input" +import { DEFAULT_MODEL_CONFIG } from "~/llm/default-config" +import type { Message, ModelConfig, ProgressItem } from "~/src/types" + +function Chat() { + const [inputText, setInputText] = useState("") + const [messages, setMessages] = useState([]) + const messagesEndRef = useRef(null) + const [progressItems, setProgressItems] = useState([]) + + const [modelConfig, setModelConfig] = useStorage( + "model_config", + DEFAULT_MODEL_CONFIG + ) + + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.status === "initiate") { + console.log("Model initiate:", message) + setProgressItems((prev) => [...prev, message.data]) + + setMessages((prev) => + prev.map((m) => { + if (m.content === "Thinking...") { + return { ...m, content: "Loading model..." } + } + return m + }) + ) + } else if (message.status === "progress") { + // Handle model progress + setProgressItems((prev) => + prev.map((item) => { + if (item.file === message.data.file) { + return { ...item, ...message.data } + } + return item + }) + ) + } else if (message.status === "done") { + // Handle model done + console.log("Model done:", message) + setProgressItems((prev) => { + const newItems = prev.filter( + (item) => item.file !== message.data.file + ) + if (newItems.length === 0) { + setMessages((prev) => + prev.map((m) => { + if (["Thinking...", "Loading model..."].includes(m.content)) { + return { ...m, content: "Thinking..." } + } + return m + }) + ) + } + return newItems + }) + } else if (message.status === "assistant") { + console.log("Assistant:", message) + setMessages((prev) => + prev.map((m) => { + if (["Thinking...", "Loading model..."].includes(m.content)) { + return { ...m, content: message.data.text } + } + return m + }) + ) + } else if (message.status === "update") { + setMessages((prev) => { + const response = message.data + const metadata = `${response.numTokens} tokens in ${response.latency.toFixed(0)} ms (${response.tps.toFixed(1)} tokens/sec)` + const last = prev[prev.length - 1] + const content = ["Thinking...", "Loading model..."].includes( + last.content + ) + ? response.output + : last.content + response.output + return [ + ...prev.slice(0, -1), + { + ...last, + content: content, + role: "assistant", + metadata + } + ] + }) + } + }) + }, []) + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [messages]) + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + setInputText(event.target.value) + }, + [] + ) + + const handleSubmitOnText = (text: string) => { + setIsLoading(true) + + const promptMessages = messages + .map((m) => ({ + role: m.role, + content: m.content + })) + .concat([ + { + role: "user", + content: text + } + ]) + const message = { + action: "generate", + messages: promptMessages + } + + const pendingMessages = promptMessages.concat([ + { role: "assistant", content: "Thinking..." } + ]) + setMessages(pendingMessages) + setInputText("") + + chrome.runtime.sendMessage(message, (response) => { + setIsLoading(false) + }) + } + + const handleSubmit = useCallback(() => { + if (!inputText.trim()) return + if (isLoading) return + + handleSubmitOnText(inputText) + }, [inputText]) + + const handleKeyPress = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleSubmit() + } + }, + [handleSubmit] + ) + + return ( +
+ {/* Fixed Header */} + { + setMessages([]) + setInputText("") + }} + hasChat={messages.length > 0} + /> + + {/* Fixed Progress Bar */} + {progressItems.length > 0 && ( + + )} + + {/* Scrollable Messages Container */} + {messages.length > 0 ? ( + + ) : ( + { + setInputText(example) + handleSubmitOnText(example) + }} + /> + )} + + {/* Fixed Footer */} +
+ + +
+
+ ) +} + +export default Chat diff --git a/chrome-extension-plasmo/components/ChatCopyButton.tsx b/chrome-extension-plasmo/components/ChatCopyButton.tsx new file mode 100644 index 00000000..34d721cd --- /dev/null +++ b/chrome-extension-plasmo/components/ChatCopyButton.tsx @@ -0,0 +1,27 @@ +import { Check, Copy } from "lucide-react" +import { useState } from "react" + +function ChatCopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false) + + const handleCopy = async (text: string) => { + setCopied(true) + await navigator.clipboard.writeText(text) + setTimeout(() => setCopied(false), 1000) + } + + return ( + + ) +} + +export default ChatCopyButton diff --git a/chrome-extension-plasmo/components/ChatExamples.tsx b/chrome-extension-plasmo/components/ChatExamples.tsx new file mode 100644 index 00000000..27492200 --- /dev/null +++ b/chrome-extension-plasmo/components/ChatExamples.tsx @@ -0,0 +1,52 @@ +import { Terminal } from "lucide-react" +import { useState } from "react" + +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert" + +function ChatExamples({ + onExampleClick +}: { + onExampleClick: (example: string) => void +}) { + const [examples, setExamples] = useState([ + "Give me some tips to improve my time management skills.", + "What is the difference between AI and ML?", + "Write python code to compute the nth fibonacci number." + ]) + + const handleExampleClick = (example: string) => { + onExampleClick(example) + } + + return ( +
+ + + Hello Local World! + + You can chat with large language models locally. No internet + connection needed. + + + {examples.map((example, index) => ( +
handleExampleClick(example)} + className="text-sm bg-blue-100 rounded-md p-2 cursor-pointer hover:bg-blue-200"> + {example} +
+ ))} +
+ ) +} + +export default ChatExamples diff --git a/chrome-extension-plasmo/components/ChatHeader.tsx b/chrome-extension-plasmo/components/ChatHeader.tsx new file mode 100644 index 00000000..5bf88ccd --- /dev/null +++ b/chrome-extension-plasmo/components/ChatHeader.tsx @@ -0,0 +1,210 @@ +import { + ArrowLeftRight, + Bot, + EllipsisVertical, + Eraser, + Github, + History, + Logs, + Milestone, + Pencil, + Power, + Settings2 +} from "lucide-react" +import React, { useEffect, useState } from "react" + +import { Button } from "~/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "~/components/ui/dropdown-menu" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "~/components/ui/tooltip" + +import ChangeModelForm from "./ChangeModelForm" +import GenerationConfigForm from "./GenerationConfigForm" +import ModelRegistryForm from "./ModelRegistryForm" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from "./ui/dialog" + +function ChatHeader({ + modelName, + onNewChat, + hasChat +}: { + modelName: string + onNewChat: () => void + hasChat: boolean +}) { + const [version, setVersion] = useState("0.0.0") + const [dialogMode, setDialogMode] = useState< + "generation_settings" | "change_model" | "model_registry" + >("generation_settings") + + useEffect(() => { + if (chrome?.runtime?.getManifest) { + const manifest = chrome.runtime.getManifest() + setVersion(manifest.version) + } + }, []) + + return ( +
+

+ Chat with{" "} + + {modelName.split("/")[1]} + +

+
+ + + + + + New Chat + + + + + + + + + Generation + + + { + event.preventDefault() + setDialogMode("generation_settings") + }}> + + Change Settings + + + Model + + { + chrome.runtime.reload() + }}> + + Restart Model + + + { + event.preventDefault() + setDialogMode("change_model") + }}> + + Change Model + + + + { + event.preventDefault() + setDialogMode("model_registry") + }}> + + Model Registry + + + Others + + + + Version {version} + + + window.open( + "https://github.com/tantara/transformers.js-chrome", + "_blank" + ) + }> + + Change Logs + + + window.open( + "https://github.com/tantara/transformers.js-chrome", + "_blank" + ) + }> + + Repository + + + + + {dialogMode === "generation_settings" && ( + + Generation Settings + + + + + )} + {dialogMode === "change_model" && ( + + Change Model + + + + + )} + {dialogMode === "model_registry" && ( + + Model Registry + + + + + )} + + +
+
+ ) +} + +export default ChatHeader diff --git a/chrome-extension-plasmo/components/ChatMessages.tsx b/chrome-extension-plasmo/components/ChatMessages.tsx new file mode 100644 index 00000000..ff34c855 --- /dev/null +++ b/chrome-extension-plasmo/components/ChatMessages.tsx @@ -0,0 +1,97 @@ +import DOMPurify from "dompurify" +import { marked } from "marked" + +import ChatCopyButton from "~/components/ChatCopyButton" +import { cn } from "~/lib/utils" +import type { Message } from "~/src/types" + +function ChatMessages({ + messages, + messagesEndRef +}: { + messages: Message[] + messagesEndRef: React.RefObject +}) { + const render = (text: string) => { + return DOMPurify.sanitize(marked.parse(text, { async: false })) + } + + return ( +
+ {messages.map((msg, index) => ( +
+
+
+

+ +

+
+ {msg.role === "assistant" && } +
+ {msg.metadata && ( +
+ {msg.metadata} +
+ )} +
+ ))} +
+
+ ) +} + +export default ChatMessages diff --git a/chrome-extension-plasmo/components/ChatProgress.tsx b/chrome-extension-plasmo/components/ChatProgress.tsx new file mode 100644 index 00000000..98e9daf9 --- /dev/null +++ b/chrome-extension-plasmo/components/ChatProgress.tsx @@ -0,0 +1,30 @@ +import { formatBytes } from "~/lib/formatter" +import type { ProgressItem } from "~/src/types" + +function Progress({ file, progress, total }) { + progress ??= 0 + return ( +
+
+ {file} ({progress.toFixed(2)}% + {isNaN(total) ? "" : ` of ${formatBytes(total)}`}) +
+
+ ) +} + +function ChatProgress({ progressItems }: { progressItems: ProgressItem[] }) { + return ( +
+
+ {progressItems.map((item) => ( + + ))} +
+
+ ) +} + +export default ChatProgress diff --git a/chrome-extension-plasmo/components/GenerationConfigForm.tsx b/chrome-extension-plasmo/components/GenerationConfigForm.tsx new file mode 100644 index 00000000..9f5cf142 --- /dev/null +++ b/chrome-extension-plasmo/components/GenerationConfigForm.tsx @@ -0,0 +1,139 @@ +import type { GenerationConfig } from "@huggingface/transformers/types/generation/configuration_utils" +import { useState } from "react" + +import { useStorage } from "@plasmohq/storage/hook" + +import { DEFAULT_GENERATION_CONFIG } from "~/llm/default-config" + +import { Alert, AlertDescription } from "./ui/alert" +import { Input } from "./ui/input" +import { Label } from "./ui/label" +import { Switch } from "./ui/switch" + +function GenerationConfigForm() { + const [config, setConfig] = useStorage( + "generation_config", + DEFAULT_GENERATION_CONFIG + ) + const [updated, setUpdated] = useState(false) + + const handleChange = async ( + field: keyof GenerationConfig, + value: number | boolean + ) => { + await setConfig({ + ...config, + [field]: value + }) + setUpdated(true) + } + + return ( +
+
+ + handleChange("do_sample", checked)} + /> +
+ +
+ + handleChange("top_k", Number(e.target.value))} + className="max-w-24" + /> +
+ +
+ + handleChange("temperature", Number(e.target.value))} + className="max-w-24" + /> +
+ +
+ + handleChange("top_p", Number(e.target.value))} + className="max-w-24" + /> +
+ +
+ + + handleChange("max_new_tokens", Number(e.target.value)) + } + className="max-w-24" + /> +
+ +
+ + + handleChange("repetition_penalty", Number(e.target.value)) + } + className="max-w-24" + /> +
+ + {updated && ( + + + Generation config updated. Please send a message to see the changes. + + + )} +
+ ) +} + +export default GenerationConfigForm diff --git a/chrome-extension-plasmo/components/ModelRegistryForm.tsx b/chrome-extension-plasmo/components/ModelRegistryForm.tsx new file mode 100644 index 00000000..bb77fd03 --- /dev/null +++ b/chrome-extension-plasmo/components/ModelRegistryForm.tsx @@ -0,0 +1,88 @@ +import { TrashIcon } from "lucide-react" +import { useEffect, useState } from "react" + +import { formatBytes } from "~/lib/formatter" + +import { Badge } from "./ui/badge" +import { Button } from "./ui/button" +import { ScrollArea } from "./ui/scroll-area" +import { Separator } from "./ui/separator" + +function ModelRegistryForm() { + const targetCacheName = "transformers-cache" + const listCacheStorage = async (targetCacheName: string) => { + try { + const cacheNames = await caches.keys() + + for (const cacheName of cacheNames) { + const cache = await caches.open(cacheName) + const requests = await cache.keys() + console.log(`Cache: ${cacheName}`) + + if (cacheName === targetCacheName) { + const cacheDetails = await Promise.all( + requests.map(async (request) => { + const response = await cache.match(request) + const blob = await response.blob() + return { + url: request.url, + size: blob.size, + sizeFormatted: formatBytes(blob.size) + } + }) + ) + return cacheDetails + } + } + + return [] + } catch (error) { + console.error("Error accessing cache:", error) + } + } + + const [cachedFiles, setCachedFiles] = useState< + { url: string; size: number; sizeFormatted: string }[] + >([]) + + useEffect(() => { + listCacheStorage(targetCacheName).then((requests) => + setCachedFiles(requests) + ) + }, []) + + const handleDelete = async (url: string) => { + const cache = await caches.open(targetCacheName) + await cache.delete(url) + setCachedFiles((prev) => prev.filter((file) => file.url !== url)) + } + + return ( + +

+ {cachedFiles.length} files cached.{" "} + {formatBytes(cachedFiles.reduce((acc, file) => acc + file.size, 0))} in + total. +

+ +
+ {cachedFiles.map((file) => ( + <> +
+ {file.url} {file.sizeFormatted}{" "} + +
+ + + ))} +
+
+ ) +} + +export default ModelRegistryForm diff --git a/chrome-extension-plasmo/components/ui/accordion.tsx b/chrome-extension-plasmo/components/ui/accordion.tsx new file mode 100644 index 00000000..d718c907 --- /dev/null +++ b/chrome-extension-plasmo/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "~/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/chrome-extension-plasmo/components/ui/alert.tsx b/chrome-extension-plasmo/components/ui/alert.tsx new file mode 100644 index 00000000..6c72a38b --- /dev/null +++ b/chrome-extension-plasmo/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/chrome-extension-plasmo/components/ui/badge.tsx b/chrome-extension-plasmo/components/ui/badge.tsx new file mode 100644 index 00000000..5e2b7aca --- /dev/null +++ b/chrome-extension-plasmo/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/chrome-extension-plasmo/components/ui/breadcrumb.tsx b/chrome-extension-plasmo/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..2ca8b012 --- /dev/null +++ b/chrome-extension-plasmo/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "~/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>