From 22c26b4b5dbc9d8b1d9fcab57aab0cf752b830ba Mon Sep 17 00:00:00 2001 From: Nestor Qin Date: Fri, 24 May 2024 18:14:41 -0400 Subject: [PATCH] [Docs] Update README (#419) This PR updates README by: - Add WebLLM links and demo video - Use badges than links for NPM, Web App, and Discord Invitation - Add MLC logo - Style update Preview: https://github.com/Neet-Nestor/web-llm/tree/readme --- .lintstagedrc.json | 5 +- README.md | 131 ++++++++++-------- .../img/logo/mlc-logo-with-text-landscape.png | Bin 0 -> 18673 bytes 3 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 site/img/logo/mlc-logo-with-text-landscape.png diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 88eb34f5..089edcec 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,6 +1,3 @@ { - "./**/*.{js,ts,jsx,tsx,json,html,css,md}": [ - "eslint --fix", - "prettier --write" - ] + "./**/*.{js,ts,jsx,tsx,json,html,css}": ["eslint --fix", "prettier --write"] } diff --git a/README.md b/README.md index 8894b81a..283b810a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,16 @@ -[discord-url]: https://discord.gg/9Xpy2HGBuD +
-# Web LLM -| [NPM Package](https://www.npmjs.com/package/@mlc-ai/web-llm) | [Get Started](#get-started) | [Examples](examples) | [Documentation](https://mlc.ai/mlc-llm/docs/deploy/javascript.html) | [MLC LLM](https://github.com/mlc-ai/mlc-llm) | [Discord][discord-url] | +[![NPM Package](./site/img/logo/mlc-logo-with-text-landscape.png)](https://mlc.ai) + +# WebLLM + +[![NPM Package](https://img.shields.io/badge/NPM_Package-Published-cc3534)](https://www.npmjs.com/package/@mlc-ai/web-llm) +[!["WebLLM Chat Deployed"](https://img.shields.io/badge/WebLLM_Chat-Deployed-%2332a852?logo=github)](https://chat.webllm.ai/) +[![Join Discoard](https://img.shields.io/badge/Join-Discord-7289DA?logo=discord&logoColor=white)]("https://discord.gg/9Xpy2HGBuD") + +| [WebLLM Chat](https://chat.webllm.ai/) | [Get Started](#get-started) | [Examples](examples) | [Documentation](https://mlc.ai/mlc-llm/docs/deploy/javascript.html) | [MLC LLM](https://github.com/mlc-ai/mlc-llm) | + +
WebLLM is a modular and customizable javascript package that directly brings language model chats directly onto web browsers with hardware acceleration. @@ -13,13 +22,16 @@ including json-mode, function-calling, streaming, etc. We can bring a lot of fun opportunities to build AI assistants for everyone and enable privacy while enjoying GPU acceleration. -**[Check out our demo webpage to try it out!](https://webllm.mlc.ai/)** You can use WebLLM as a base [npm package](https://www.npmjs.com/package/@mlc-ai/web-llm) and build your own web application on top of it by following the [documentation](https://mlc.ai/mlc-llm/docs/deploy/javascript.html) and checking out [Get Started](#get-started). -This project is a companion project of [MLC LLM](https://github.com/mlc-ai/mlc-llm), -which runs LLMs natively on iPhone and other native local environments. +This project is a companion project of [MLC LLM](https://github.com/mlc-ai/mlc-llm), which runs LLMs natively on iPhone and other native local environments. + +
+ +**[Check out WebLLM Chat to try it out!](https://chat.webllm.ai/)** +[WebLLM Chat Demo Video](https://github.com/mlc-ai/web-llm-chat/assets/23090573/f700e27e-bb88-4068-bc8b-8a33ea5a4300) - +
## Get Started @@ -40,11 +52,11 @@ async function main() { const selectedModel = "Llama-3-8B-Instruct-q4f32_1"; const engine: webllm.MLCEngineInterface = await webllm.CreateMLCEngine( selectedModel, - /*engineConfig=*/{ initProgressCallback: initProgressCallback } + /*engineConfig=*/ { initProgressCallback: initProgressCallback }, ); const reply0 = await engine.chat.completions.create({ - messages: [{ "role": "user", "content": "Tell me about Pittsburgh." }] + messages: [{ role: "user", content: "Tell me about Pittsburgh." }], }); console.log(reply0); console.log(await engine.runtimeStatsText()); @@ -58,7 +70,7 @@ Note that if you need to separate the instantiation of `webllm.MLCEngine` from l ```typescript const engine: webllm.MLCEngineInterface = await webllm.CreateMLCEngine( selectedModel, - /*engineConfig=*/{ initProgressCallback: initProgressCallback } + /*engineConfig=*/ { initProgressCallback: initProgressCallback }, ); ``` @@ -71,26 +83,28 @@ await engine.reload(selectedModel, chatConfig, appConfig); ``` ### CDN Delivery + Thanks to [jsdelivr.com](https://www.jsdelivr.com/package/npm/@mlc-ai/web-llm), the following Javascript code should work out-of-the-box on sites like [jsfiddle.net](https://jsfiddle.net/): ```javascript -import * as webllm from 'https://esm.run/@mlc-ai/web-llm'; +import * as webllm from "https://esm.run/@mlc-ai/web-llm"; async function main() { const initProgressCallback = (report) => { console.log(report.text); }; const selectedModel = "TinyLlama-1.1B-Chat-v0.4-q4f16_1-1k"; - const engine = await webllm.CreateMLCEngine( - selectedModel, - {initProgressCallback: initProgressCallback} - ); + const engine = await webllm.CreateMLCEngine(selectedModel, { + initProgressCallback: initProgressCallback, + }); const reply = await engine.chat.completions.create({ - messages: [{ - "role": "user", - "content": "Tell me about Pittsburgh." - }] + messages: [ + { + role: "user", + content: "Tell me about Pittsburgh.", + }, + ], }); console.log(reply); console.log(await engine.runtimeStatsText()); @@ -129,22 +143,22 @@ import * as webllm from "@mlc-ai/web-llm"; async function main() { // Use a WebWorkerMLCEngine instead of MLCEngine here - const engine: webllm.MLCEngineInterface = await webllm.CreateWebWorkerMLCEngine( - /*worker=*/new Worker( - new URL('./worker.ts', import.meta.url), - { type: 'module' } - ), - /*modelId=*/selectedModel, - /*engineConfig=*/{ initProgressCallback: initProgressCallback } - ); + const engine: webllm.MLCEngineInterface = + await webllm.CreateWebWorkerMLCEngine( + /*worker=*/ new Worker(new URL("./worker.ts", import.meta.url), { + type: "module", + }), + /*modelId=*/ selectedModel, + /*engineConfig=*/ { initProgressCallback: initProgressCallback }, + ); // everything else remains the same } ``` ### Use Service Worker -WebLLM comes with API support for ServiceWorker so you can hook the generation process -into a service worker to avoid reloading the model in every page visit and optimize +WebLLM comes with API support for ServiceWorker so you can hook the generation process +into a service worker to avoid reloading the model in every page visit and optimize your application's offline experience. We first create a service worker script that created a MLCEngine and hook it up to a handler @@ -163,9 +177,8 @@ let handler: ServiceWorkerMLCEngineHandler; self.addEventListener("activate", function (event) { handler = new ServiceWorkerMLCEngineHandler(engine); - console.log("Service Worker is ready") + console.log("Service Worker is ready"); }); - ``` Then in the main logic, we register the service worker and then create the engine using @@ -175,15 +188,15 @@ Then in the main logic, we register the service worker and then create the engin // main.ts if ("serviceWorker" in navigator) { navigator.serviceWorker.register( - /*workerScriptURL=*/new URL("sw.ts", import.meta.url), - { type: "module" } + /*workerScriptURL=*/ new URL("sw.ts", import.meta.url), + { type: "module" }, ); } const engine: webllm.MLCEngineInterface = await webllm.CreateServiceWorkerMLCEngine( - /*modelId=*/selectedModel, - /*engineConfig=*/{ initProgressCallback: initProgressCallback } + /*modelId=*/ selectedModel, + /*engineConfig=*/ { initProgressCallback: initProgressCallback }, ); ``` @@ -200,6 +213,7 @@ You can also find examples on building chrome extension with WebLLM in [examples ## Full OpenAI Compatibility WebLLM is designed to be fully compatible with [OpenAI API](https://platform.openai.com/docs/api-reference/chat). Thus, besides building simple chat bot, you can also have the following functionalities with WebLLM: + - [streaming](examples/streaming): return output as chunks in real-time in the form of an AsyncGenerator - [json-mode](examples/json-mode): efficiently ensure output is in json format, see [OpenAI Reference](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) for more. - [function-calling](examples/function-calling): function calling with fields `tools` and `tool_choice`. @@ -210,6 +224,7 @@ WebLLM is designed to be fully compatible with [OpenAI API](https://platform.ope We export all supported models in `webllm.prebuiltAppConfig`, where you can see a list of models that you can simply call `const engine: webllm.MLCEngineInterface = await webllm.CreateMLCEngine(anyModel)` with. Prebuilt models include: + - Llama-2 - Llama-3 - Gemma @@ -258,7 +273,7 @@ async main() { // The chat will also load the model library from "/url/to/myllama3b.wasm", // assuming that it is compatible to the model in myLlamaUrl. const engine = await webllm.CreateMLCEngine( - "MyLlama-3b-v1-q4f32_0", + "MyLlama-3b-v1-q4f32_0", /*engineConfig=*/{ chatOpts: chatOpts, appConfig: appConfig } ); } @@ -269,7 +284,6 @@ not necessarily a new model (e.g. `NeuralHermes-Mistral` can reuse `Mistral`'s model library). For examples on how a model library can be shared by different model variants, see `prebuiltAppConfig`. - ## Build WebLLM Package From Source NOTE: you don't need to build by yourself unless you would @@ -278,36 +292,37 @@ like to change the WebLLM package. To simply use the npm, follow [Get Started](# WebLLM package is a web runtime designed for [MLC LLM](https://github.com/mlc-ai/mlc-llm). 1. Install all the prerequisites for compilation: - 1. [emscripten](https://emscripten.org). It is an LLVM-based compiler that compiles C/C++ source code to WebAssembly. - - Follow the [installation instruction](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended) to install the latest emsdk. - - Source `emsdk_env.sh` by `source path/to/emsdk_env.sh`, so that `emcc` is reachable from PATH and the command `emcc` works. - 4. Install jekyll by following the [official guides](https://jekyllrb.com/docs/installation/). It is the package we use for website. This is not needed if you're using nextjs (see next-simple-chat in the examples). - 5. Install jekyll-remote-theme by command. Try [gem mirror](https://gems.ruby-china.com/) if install blocked. - ```shell - gem install jekyll-remote-theme - ``` - We can verify the successful installation by trying out `emcc` and `jekyll` in terminal, respectively. + + 1. [emscripten](https://emscripten.org). It is an LLVM-based compiler that compiles C/C++ source code to WebAssembly. + - Follow the [installation instruction](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended) to install the latest emsdk. + - Source `emsdk_env.sh` by `source path/to/emsdk_env.sh`, so that `emcc` is reachable from PATH and the command `emcc` works. + 2. Install jekyll by following the [official guides](https://jekyllrb.com/docs/installation/). It is the package we use for website. This is not needed if you're using nextjs (see next-simple-chat in the examples). + 3. Install jekyll-remote-theme by command. Try [gem mirror](https://gems.ruby-china.com/) if install blocked. + `shell +gem install jekyll-remote-theme +` + We can verify the successful installation by trying out `emcc` and `jekyll` in terminal, respectively. 2. Setup necessary environment - Prepare all the necessary dependencies for web build: - ```shell - ./scripts/prep_deps.sh - ``` + Prepare all the necessary dependencies for web build: + + ```shell + ./scripts/prep_deps.sh + ``` 3. Buld WebLLM Package - ```shell - npm run build - ``` + ```shell + npm run build + ``` 4. Validate some of the sub-packages - You can then go to the subfolders in [examples](examples) to validate some of the sub-packages. - We use Parcelv2 for bundling. Although Parcel is not very good at tracking parent directory - changes sometimes. When you make a change in the WebLLM package, try to edit the `package.json` - of the subfolder and save it, which will trigger Parcel to rebuild. - + You can then go to the subfolders in [examples](examples) to validate some of the sub-packages. + We use Parcelv2 for bundling. Although Parcel is not very good at tracking parent directory + changes sometimes. When you make a change in the WebLLM package, try to edit the `package.json` + of the subfolder and save it, which will trigger Parcel to rebuild. ## Links diff --git a/site/img/logo/mlc-logo-with-text-landscape.png b/site/img/logo/mlc-logo-with-text-landscape.png new file mode 100644 index 0000000000000000000000000000000000000000..57760bee895e6e618087a09395a9d11f5b9deef6 GIT binary patch literal 18673 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