diff --git a/README.md b/README.md index 008d735..b95bd2b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Local-Bench -**A [Companion Intelligence](https://ci.computer/) app for benchmarking local LLMs — and comparing them by intelligence.** +A [Companion Intelligence](https://ci.computer/) app for benchmarking local LLMs and comparing them by intelligence. -Local-Bench measures the token-generation speed of models you run locally (via [Ollama](https://ollama.ai/) or AMD STRIX Halo / llama.cpp) and presents the results in a clean, local-first web dashboard. It also ships a curated Companion model catalog ranked by the **Artificial Analysis Intelligence Index**, so you can weigh capability against the throughput your hardware actually delivers. +Local-Bench measures how the language models on your own machine perform. It runs a prompt against one or more models served by [Ollama](https://ollama.ai/), records how fast each model generates text, and shows the results in a local web dashboard with tables, charts, and side by side response comparison. You can also run it from the command line and export a PDF report. + +Everything runs locally. Your prompts and the model responses stay on your machine. ![Local-Bench dashboard](docs/screenshots/00-hero.png) @@ -18,24 +20,31 @@ Local-Bench measures the token-generation speed of models you run locally (via [ | --- | --- | | ![Run a benchmark](docs/screenshots/03-run-benchmark.png) | ![Detailed results](docs/screenshots/05-results.png) | -> Full-page view: [`docs/screenshots/01-overview.png`](docs/screenshots/01-overview.png) +Full page view: [`docs/screenshots/01-overview.png`](docs/screenshots/01-overview.png) + +## What it does -## Features +- Benchmarks any model installed in Ollama and reports tokens per second, total tokens, and duration. +- Shows a curated catalog of popular models with sizes, context windows, and an intelligence score, so you can compare capability alongside speed. +- Lets you pick a built in prompt, edit it, or write your own before running. +- Shows two model responses at a time so you can read them next to each other. +- Captures your system specs (CPU, memory, OS, GPU) with every run. +- Exports a PDF report with the results table, your system summary, and the full model responses. +- Includes an optional path for benchmarking AMD Strix Halo GPUs with llama.cpp. -- 📊 Benchmark multiple local LLMs and measure **tokens per second** for each -- 🧠 **Model intelligence catalog** — curated Companion models ranked by the Artificial Analysis Intelligence Index, shown alongside size, context window, and modality -- 🧪 A broad prompt library (coding, reasoning, extraction, planning, summarization, translation, instruction-following) plus custom prompts -- 💾 Results stored in SQLite (`benchmark_data.db`) and CSV (`benchmark_results.csv`) -- 💻 Captures system specifications (CPU, memory, OS, GPU, STRIX Halo) with every run -- 🎮 **AMD STRIX Halo** GPU support for `llama.cpp` benchmarking -- 🪶 **Local-first** — Chart.js is vendored into the repo, so the dashboard renders fully offline (it gracefully falls back to CSS bar charts if scripting is unavailable) -- 🎨 Styled to match the **Companion Intelligence Hub design system** — **light & dark** colorways (follows your system theme, with a toggle), teal accents, Montserrat, and the official CI logo banner +## Why it is useful -## Prerequisites +1. Choose the right model for your hardware. See which models run fast enough on your machine before you build them into a workflow. +2. Balance speed against quality. Compare tokens per second against the intelligence score to find the best tradeoff for your needs. +3. Measure a hardware change. Benchmark before and after adding a GPU or more memory to see how much faster inference gets. +4. Compare two models on the same prompt. Read both answers side by side to judge which model responds better for your task. +5. Share findings. Export a PDF with the numbers, system specs, and responses to send to a teammate or attach to a writeup. -- [Node.js](https://nodejs.org/) (v18 or higher) -- **For Ollama benchmarks:** [Ollama](https://ollama.ai/) running locally with at least one model pulled -- **For AMD STRIX Halo benchmarks:** an AMD Ryzen AI Max "Strix Halo" APU — see [STRIX_HALO.md](STRIX_HALO.md) +## Requirements + +- [Node.js](https://nodejs.org/) version 18 or higher. +- [Ollama](https://ollama.ai/) running locally with at least one model pulled. +- Optional: an AMD Ryzen AI Max "Strix Halo" machine for the llama.cpp GPU path. See [STRIX_HALO.md](STRIX_HALO.md). ## Quick start @@ -44,41 +53,43 @@ git clone https://github.com/companionintelligence/Local-Bench.git cd Local-Bench npm install -# make sure Ollama is running (default http://localhost:11434) +# start Ollama if it is not already running (default http://localhost:11434) ollama serve -# build + start the dashboard +# pull a model or two if you have not already +ollama pull llama3.2:3b +ollama pull qwen3:8b + +# build and start the dashboard npm start # open http://localhost:3000 ``` -Don't have Ollama running yet? The dashboard still loads — it shows the curated catalog and intelligence scores, and degrades gracefully (see [First run / no Ollama](#first-run--no-ollama)). +If Ollama is not running yet, the dashboard still loads. It shows the curated catalog and intelligence scores and tells you to start Ollama when you want to run a benchmark. + +## Using the web dashboard + +```bash +npm start # http://localhost:3000 +PORT=8080 npm start # custom port +``` -## Usage +In the dashboard you can select installed models, choose and edit a prompt, run a benchmark, watch the charts and results update, compare two responses, and export a PDF. -### Run benchmarks from the CLI +## Running from the command line ```bash -# Benchmark the curated catalog +# benchmark the curated catalog npm run benchmark -# Benchmark specific installed models +# benchmark specific installed models node dist/benchmark.js llama3.2:3b qwen3:8b gemma3:4b -# Point at a non-default Ollama +# point at a non default Ollama OLLAMA_API_URL=http://192.168.1.50:11434 npm run benchmark ``` -### Run benchmarks from the web UI - -```bash -npm start # http://localhost:3000 -PORT=8080 npm start # custom port -``` - -The dashboard lets you pick installed models, choose a prompt (or write your own), run benchmarks, and watch results, charts, statistics, and system specs update live. - -### AMD STRIX Halo +## AMD Strix Halo benchmarks ```bash npm run strix-halo detect @@ -86,165 +97,67 @@ npm run strix-halo setup llama-rocm-7.2 npm run strix-halo benchmark /path/to/model.gguf --toolbox llama-rocm-7.2 ``` -Full guide: [STRIX_HALO.md](STRIX_HALO.md). - -## Model intelligence scores - -Each curated model carries an **intelligence score** sourced from the -[Artificial Analysis Intelligence Index](https://artificialanalysis.ai/) — a -composite benchmark (MMLU-Pro, GPQA Diamond, LiveCodeBench, AIME, and more) -scored roughly 0–100 where higher is more capable. The dashboard surfaces it as -an `IQ` badge on each model and as a ranked "Model intelligence" list. +See [STRIX_HALO.md](STRIX_HALO.md) for the full guide. -A few notes on the data: +## Run it with Docker -- Scores are a **snapshot** (currently `2026-06`) and are version-sensitive — the index evolves as new evaluations and models are added. -- **Vision-only** (`-vl`) variants and very small models are not individually rated by the index and appear as **"Not rated" / `IQ —`**. -- The values are a best-effort public reference and are intended to be **easy to override** with Companion's own curated numbers. They live in a single place — the `intelligenceIndex` field of `SUPPORTED_OLLAMA_MODELS` in [`src/benchmark.ts`](src/benchmark.ts) — which both the CLI and UI read from. The attribution shown in the UI comes from `INTELLIGENCE_INDEX_SOURCE` / `INTELLIGENCE_INDEX_URL` / `INTELLIGENCE_INDEX_AS_OF` in the same file. - -## Docker / Companion Intelligence Hub - -Local-Bench is published as a public, multi-architecture (amd64 + arm64) container image and runs as a first-party app on the Companion Intelligence Hub. - -**Image:** `ghcr.io/companionintelligence/ci-local-bench:latest` +Local-Bench is published as a multi architecture (amd64 and arm64) container image and runs as a first party app on the Companion Intelligence Hub. ```bash docker run -d -p 3000:3000 ghcr.io/companionintelligence/ci-local-bench:latest # open http://localhost:3000 ``` -Point it at an Ollama server with `OLLAMA_API_URL` (inside a container, `localhost` is the container itself): +Point it at your Ollama server with `OLLAMA_API_URL`. Inside a container, `localhost` is the container itself, so use the host address: ```bash -# Ollama on the Docker host docker run -d -p 3000:3000 \ -e OLLAMA_API_URL=http://host.docker.internal:11434 \ ghcr.io/companionintelligence/ci-local-bench:latest - -# Ollama on the LAN -docker run -d -p 3000:3000 \ - -e OLLAMA_API_URL=http://192.168.1.50:11434 \ - ghcr.io/companionintelligence/ci-local-bench:latest ``` -Benchmark data (`benchmark_data.db` + `benchmark_results.csv`) is written to the working directory (`/app` in the container). Mount a volume there to persist results across restarts. +Benchmark data is written to the working directory (`/app` in the container). Mount a volume there to keep results across restarts. -### First run / no Ollama +## Where results are stored -The dashboard degrades gracefully when there's no data and no reachable Ollama: +- `benchmark_results.csv` is a flat record of every run. +- `benchmark_data.db` is a SQLite database with results and system specs. -- It loads and shows a friendly welcome instead of a red error banner. -- The **Model intelligence** catalog still renders (scores come from the bundled catalog, not Ollama). -- The model picker falls back to the curated catalog and notes that Ollama is unavailable; `GET /api/models` returns HTTP `503` with the catalog body. -- System specs, statistics, and the chart show neutral "run a benchmark" placeholders until results exist. +## Model intelligence scores -## API endpoints - -| Endpoint | Description | -| --- | --- | -| `GET /api/models` | Curated catalog merged with installed Ollama models (incl. `intelligenceIndex`). `503` + catalog body if Ollama is unreachable. | -| `GET /api/prompts` | The benchmark prompt library | -| `GET /api/results` | All stored benchmark results | -| `GET /api/system-specs` | Latest captured system specifications | -| `GET /api/results-with-specs?limit=N` | Results joined with their system specs | -| `GET /api/meta` | Intelligence-score attribution (source, URL, snapshot date) | -| `POST /api/run-benchmark` | Run a benchmark `{ models: string[], promptId?, customPrompt? }` | - -## Output (CSV) - -```csv -Model,Tokens Per Second,Total Tokens,Duration (s),Timestamp,Status -gemma3:4b,78.42,412,5.25,2026-06-11T20:36:02.000Z,Success -qwen3:8b,52.31,498,9.52,2026-06-11T20:38:02.000Z,Success -``` +Each curated model carries an intelligence score from the [Artificial Analysis Intelligence Index](https://artificialanalysis.ai/), a composite benchmark scored roughly 0 to 100 where higher is more capable. The dashboard shows it as an IQ badge on each model and as a ranked list. Scores are a snapshot and can change as the index evolves. Vision only and very small models are not individually rated and appear as "Not rated". The full catalog is browsable in the app and defined in [`src/benchmark.ts`](src/benchmark.ts). ## Configuration Edit [`src/benchmark.ts`](src/benchmark.ts) to customize: -- `OLLAMA_API_URL` — Ollama endpoint (env var, default `http://localhost:11434`) -- `TEST_PROMPTS` — the benchmark prompt library -- `SUPPORTED_OLLAMA_MODELS` — the curated catalog, including each model's `intelligenceIndex` +- `OLLAMA_API_URL`, the Ollama endpoint (also an environment variable, default `http://localhost:11434`). +- `TEST_PROMPTS`, the benchmark prompt library. +- `SUPPORTED_OLLAMA_MODELS`, the curated catalog and each model's intelligence score. ## Testing ```bash -npm test # Jest unit tests (server, benchmark, database, system specs, STRIX Halo) +npm test # Jest unit tests npm run test:coverage -npm run build # typecheck + emit to dist/ +npm run build # typecheck and emit to dist/ ``` -The repository is set up for automated testing: - -- **Unit tests** cover the API, the catalog/intelligence data, CSV/DB writes, and system-spec collection. -- **Container smoke test** — [`tests/container/`](tests/container) is a self-contained Playwright harness that boots the published image and asserts the web UI renders, saving a screenshot artifact. It runs in the GHCR publish workflow. -- **Screenshots** in [`docs/screenshots/`](docs/screenshots) are captured against the running server (see that workflow / the Playwright harness) and embedded above. - ## Troubleshooting -**Cannot connect to Ollama:** ensure `ollama serve` is running and reachable at `http://localhost:11434`, or set `OLLAMA_API_URL`. +- Cannot connect to Ollama: make sure `ollama serve` is running and reachable at `http://localhost:11434`, or set `OLLAMA_API_URL`. +- Model not found: list installed models with `ollama list`, then pull what you need with `ollama pull `. +- A run takes too long: each model has a two minute timeout. Benchmark fewer or smaller models at a time. -**Model not found:** list installed models with `ollama list`, then `ollama pull `. +## Links -**Benchmark takes too long:** larger models are slower; there's a 2-minute per-model timeout. Benchmark fewer/smaller models at once. - -**Chart not showing:** the dashboard automatically falls back to CSS bar charts if the vendored Chart.js can't execute — no internet required. +- Documentation: https://docs.ci.computer/ +- Discord community: https://discord.com/invite/yQp9hwpzAa ## Contributing -Contributions are welcome — please open a Pull Request. - -## Supported Companion catalog - -Curated models with their Artificial Analysis Intelligence Index score (snapshot `2026-06`; `—` = not individually rated). - - - -| Model | Size | Context | Inputs | Intelligence | -| --- | --- | --- | --- | --- | -| gemma3:270m | 292MB | 32K | Text | — | -| qwen3:0.6b | 523MB | 40K | Text | — | -| gemma3:1b | 815MB | 32K | Text | — | -| deepseek-r1:1.5b | 1.1GB | 128K | Text | — | -| llama3.2:1b | 1.3GB | 128K | Text | — | -| qwen3:1.7b | 1.4GB | 40K | Text | 3 | -| qwen3-vl:2b | 1.9GB | 256K | Text, Image | — | -| llama3.2:3b | 2.0GB | 128K | Text | 4 | -| qwen3:4b | 2.5GB | 256K | Text | 6 | -| gemma3:4b | 3.3GB | 128K | Text, Image | 4 | -| qwen3-vl:4b | 3.3GB | 256K | Text, Image | — | -| deepseek-r1:7b | 4.7GB | 128K | Text | 8 | -| llama3.1:8b | 4.9GB | 128K | Text | 8 | -| deepseek-r1:8b | 5.2GB | 128K | Text | 9 | -| qwen3:8b | 5.2GB | 40K | Text | 9 | -| qwen3-vl:8b | 6.1GB | 256K | Text, Image | — | -| gemma3:12b | 8.1GB | 128K | Text, Image | 7 | -| deepseek-r1:14b | 9.0GB | 128K | Text | 13 | -| qwen3:14b | 9.3GB | 40K | Text | 11 | -| gpt-oss:20b | 14GB | 128K | Text | 24 | -| gemma3:27b | 17GB | 128K | Text, Image | 10 | -| qwen3-coder:latest | 19GB | 256K | Text | 20 | -| qwen3-coder:30b | 19GB | 256K | Text | 20 | -| qwen3:30b | 19GB | 256K | Text | 15 | -| deepseek-r1:32b | 20GB | 128K | Text | 18 | -| qwen3:32b | 20GB | 40K | Text | 15 | -| qwen3-vl:30b | 20GB | 256K | Text, Image | — | -| qwen3-vl:32b | 21GB | 256K | Text, Image | — | -| deepseek-r1:70b | 43GB | 128K | Text | 20 | -| llama3.1:70b | 43GB | 128K | Text | 16 | -| gpt-oss:120b | 65GB | 128K | Text | 33 | -| llama4:16x17b | 67GB | 10M | Text, Image | 13 | -| GLM-4.6:TQ1_0 | 84GB | 198K | Text | 30 | -| qwen3:235b | 142GB | 256K | Text | 45 | -| qwen3-vl:235b | 143GB | 256K | Text, Image | — | -| GLM-4.6:Q4_K_M | 216GB | 198K | Text | 30 | -| llama3.1:405b | 243GB | 128K | Text | 17 | -| llama4:128x17b | 245GB | 1M | Text, Image | 18 | -| qwen3-coder:480b | 290GB | 256K | Text | 24 | -| deepseek-v3.1:671b | 404GB | 160K | Text | 28 | -| deepseek-r1:671b | 404GB | 160K | Text | 27 | -| minmax m2 | 968GB | 200K | Text | 44 | - ---- - -> **Private & Confidential — Property of Lifescope Inc. Do not distribute.** +Contributions are welcome. Please open a Pull Request. + +## Confidentiality + +Private and Confidential. Property of Lifescope Inc. Do not distribute. diff --git a/assets/jspdf.umd.min.js b/assets/jspdf.umd.min.js new file mode 100644 index 0000000..5793b5a --- /dev/null +++ b/assets/jspdf.umd.min.js @@ -0,0 +1,398 @@ +/** @license + * + * jsPDF - PDF Document creation from JavaScript + * Version 2.5.2 Built on 2024-09-17T13:29:57.856Z + * CommitID 00000000 + * + * Copyright (c) 2010-2021 James Hall , https://github.com/MrRio/jsPDF + * 2015-2021 yWorks GmbH, http://www.yworks.com + * 2015-2021 Lukas Holländer , https://github.com/HackbrettXXX + * 2016-2018 Aras Abbasi + * 2010 Aaron Spike, https://github.com/acspike + * 2012 Willow Systems Corporation, https://github.com/willowsystems + * 2012 Pablo Hess, https://github.com/pablohess + * 2012 Florian Jenett, https://github.com/fjenett + * 2013 Warren Weckesser, https://github.com/warrenweckesser + * 2013 Youssef Beddad, https://github.com/lifof + * 2013 Lee Driscoll, https://github.com/lsdriscoll + * 2013 Stefan Slonevskiy, https://github.com/stefslon + * 2013 Jeremy Morel, https://github.com/jmorel + * 2013 Christoph Hartmann, https://github.com/chris-rock + * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria + * 2014 James Makes, https://github.com/dollaruw + * 2014 Diego Casorran, https://github.com/diegocr + * 2014 Steven Spungin, https://github.com/Flamenco + * 2014 Kenneth Glassey, https://github.com/Gavvers + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * Contributor(s): + * siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango, + * kim3er, mfo, alnorth, Flamenco + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).jspdf={})}(this,(function(t){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var r=function(){return"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this}();function n(){r.console&&"function"==typeof r.console.log&&r.console.log.apply(r.console,arguments)}var i={log:n,warn:function(t){r.console&&("function"==typeof r.console.warn?r.console.warn.apply(r.console,arguments):n.call(null,arguments))},error:function(t){r.console&&("function"==typeof r.console.error?r.console.error.apply(r.console,arguments):n(t))}};function a(t,e,r){var n=new XMLHttpRequest;n.open("GET",t),n.responseType="blob",n.onload=function(){l(n.response,e,r)},n.onerror=function(){i.error("could not download file")},n.send()}function o(t){var e=new XMLHttpRequest;e.open("HEAD",t,!1);try{e.send()}catch(t){}return e.status>=200&&e.status<=299}function s(t){try{t.dispatchEvent(new MouseEvent("click"))}catch(r){var e=document.createEvent("MouseEvents");e.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),t.dispatchEvent(e)}}var c,u,l=r.saveAs||("object"!==("undefined"==typeof window?"undefined":e(window))||window!==r?function(){}:"undefined"!=typeof HTMLAnchorElement&&"download"in HTMLAnchorElement.prototype?function(t,e,n){var i=r.URL||r.webkitURL,c=document.createElement("a");e=e||t.name||"download",c.download=e,c.rel="noopener","string"==typeof t?(c.href=t,c.origin!==location.origin?o(c.href)?a(t,e,n):s(c,c.target="_blank"):s(c)):(c.href=i.createObjectURL(t),setTimeout((function(){i.revokeObjectURL(c.href)}),4e4),setTimeout((function(){s(c)}),0))}:"msSaveOrOpenBlob"in navigator?function(t,r,n){if(r=r||t.name||"download","string"==typeof t)if(o(t))a(t,r,n);else{var c=document.createElement("a");c.href=t,c.target="_blank",setTimeout((function(){s(c)}))}else navigator.msSaveOrOpenBlob(function(t,r){return void 0===r?r={autoBom:!1}:"object"!==e(r)&&(i.warn("Deprecated: Expected third argument to be a object"),r={autoBom:!r}),r.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t}(t,n),r)}:function(t,n,i,o){if((o=o||open("","_blank"))&&(o.document.title=o.document.body.innerText="downloading..."),"string"==typeof t)return a(t,n,i);var s="application/octet-stream"===t.type,c=/constructor/i.test(r.HTMLElement)||r.safari,u=/CriOS\/[\d]+/.test(navigator.userAgent);if((u||s&&c)&&"object"===("undefined"==typeof FileReader?"undefined":e(FileReader))){var l=new FileReader;l.onloadend=function(){var t=l.result;t=u?t:t.replace(/^data:[^;]*;/,"data:attachment/file;"),o?o.location.href=t:location=t,o=null},l.readAsDataURL(t)}else{var h=r.URL||r.webkitURL,f=h.createObjectURL(t);o?o.location=f:location.href=f,o=null,setTimeout((function(){h.revokeObjectURL(f)}),4e4)}}); +/** + * A class to parse color values + * @author Stoyan Stefanov + * {@link http://www.phpied.com/rgb-color-parser-in-javascript/} + * @license Use it if you like it + */function h(t){var e;t=t||"",this.ok=!1,"#"==t.charAt(0)&&(t=t.substr(1,6));t={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"}[t=(t=t.replace(/ /g,"")).toLowerCase()]||t;for(var r=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(t){return[parseInt(t[1]),parseInt(t[2]),parseInt(t[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}}],n=0;n255?255:this.r,this.g=this.g<0||isNaN(this.g)?0:this.g>255?255:this.g,this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b,this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"},this.toHex=function(){var t=this.r.toString(16),e=this.g.toString(16),r=this.b.toString(16);return 1==t.length&&(t="0"+t),1==e.length&&(e="0"+e),1==r.length&&(r="0"+r),"#"+t+e+r}} +/** + * @license + * Joseph Myers does not specify a particular license for his work. + * + * Author: Joseph Myers + * Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js + * + * Modified by: Owen Leong + */ +function f(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r=p(r,n,i,a,e[0],7,-680876936),a=p(a,r,n,i,e[1],12,-389564586),i=p(i,a,r,n,e[2],17,606105819),n=p(n,i,a,r,e[3],22,-1044525330),r=p(r,n,i,a,e[4],7,-176418897),a=p(a,r,n,i,e[5],12,1200080426),i=p(i,a,r,n,e[6],17,-1473231341),n=p(n,i,a,r,e[7],22,-45705983),r=p(r,n,i,a,e[8],7,1770035416),a=p(a,r,n,i,e[9],12,-1958414417),i=p(i,a,r,n,e[10],17,-42063),n=p(n,i,a,r,e[11],22,-1990404162),r=p(r,n,i,a,e[12],7,1804603682),a=p(a,r,n,i,e[13],12,-40341101),i=p(i,a,r,n,e[14],17,-1502002290),r=g(r,n=p(n,i,a,r,e[15],22,1236535329),i,a,e[1],5,-165796510),a=g(a,r,n,i,e[6],9,-1069501632),i=g(i,a,r,n,e[11],14,643717713),n=g(n,i,a,r,e[0],20,-373897302),r=g(r,n,i,a,e[5],5,-701558691),a=g(a,r,n,i,e[10],9,38016083),i=g(i,a,r,n,e[15],14,-660478335),n=g(n,i,a,r,e[4],20,-405537848),r=g(r,n,i,a,e[9],5,568446438),a=g(a,r,n,i,e[14],9,-1019803690),i=g(i,a,r,n,e[3],14,-187363961),n=g(n,i,a,r,e[8],20,1163531501),r=g(r,n,i,a,e[13],5,-1444681467),a=g(a,r,n,i,e[2],9,-51403784),i=g(i,a,r,n,e[7],14,1735328473),r=m(r,n=g(n,i,a,r,e[12],20,-1926607734),i,a,e[5],4,-378558),a=m(a,r,n,i,e[8],11,-2022574463),i=m(i,a,r,n,e[11],16,1839030562),n=m(n,i,a,r,e[14],23,-35309556),r=m(r,n,i,a,e[1],4,-1530992060),a=m(a,r,n,i,e[4],11,1272893353),i=m(i,a,r,n,e[7],16,-155497632),n=m(n,i,a,r,e[10],23,-1094730640),r=m(r,n,i,a,e[13],4,681279174),a=m(a,r,n,i,e[0],11,-358537222),i=m(i,a,r,n,e[3],16,-722521979),n=m(n,i,a,r,e[6],23,76029189),r=m(r,n,i,a,e[9],4,-640364487),a=m(a,r,n,i,e[12],11,-421815835),i=m(i,a,r,n,e[15],16,530742520),r=v(r,n=m(n,i,a,r,e[2],23,-995338651),i,a,e[0],6,-198630844),a=v(a,r,n,i,e[7],10,1126891415),i=v(i,a,r,n,e[14],15,-1416354905),n=v(n,i,a,r,e[5],21,-57434055),r=v(r,n,i,a,e[12],6,1700485571),a=v(a,r,n,i,e[3],10,-1894986606),i=v(i,a,r,n,e[10],15,-1051523),n=v(n,i,a,r,e[1],21,-2054922799),r=v(r,n,i,a,e[8],6,1873313359),a=v(a,r,n,i,e[15],10,-30611744),i=v(i,a,r,n,e[6],15,-1560198380),n=v(n,i,a,r,e[13],21,1309151649),r=v(r,n,i,a,e[4],6,-145523070),a=v(a,r,n,i,e[11],10,-1120210379),i=v(i,a,r,n,e[2],15,718787259),n=v(n,i,a,r,e[9],21,-343485551),t[0]=S(r,t[0]),t[1]=S(n,t[1]),t[2]=S(i,t[2]),t[3]=S(a,t[3])}function d(t,e,r,n,i,a){return e=S(S(e,t),S(n,a)),S(e<>>32-i,r)}function p(t,e,r,n,i,a,o){return d(e&r|~e&n,t,e,i,a,o)}function g(t,e,r,n,i,a,o){return d(e&n|r&~n,t,e,i,a,o)}function m(t,e,r,n,i,a,o){return d(e^r^n,t,e,i,a,o)}function v(t,e,r,n,i,a,o){return d(r^(e|~n),t,e,i,a,o)}function b(t){var e,r=t.length,n=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=t.length;e+=64)f(n,y(t.substring(e-64,e)));t=t.substring(e-64);var i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e>2]|=t.charCodeAt(e)<<(e%4<<3);if(i[e>>2]|=128<<(e%4<<3),e>55)for(f(n,i),e=0;e<16;e++)i[e]=0;return i[14]=8*r,f(n,i),n}function y(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}c=r.atob.bind(r),u=r.btoa.bind(r);var w="0123456789abcdef".split("");function N(t){for(var e="",r=0;r<4;r++)e+=w[t>>8*r+4&15]+w[t>>8*r&15];return e}function L(t){return String.fromCharCode((255&t)>>0,(65280&t)>>8,(16711680&t)>>16,(4278190080&t)>>24)}function A(t){return function(t){return t.map(L).join("")}(b(t))}var x="5d41402abc4b2a76b9719d911017c592"!=function(t){for(var e=0;e>16)+(e>>16)+(r>>16)<<16|65535&r}return t+e&4294967295} +/** + * @license + * FPDF is released under a permissive license: there is no usage restriction. + * You may embed it freely in your application (commercial or not), with or + * without modifications. + * + * Reference: http://www.fpdf.org/en/script/script37.php + */function _(t,e){var r,n,i,a;if(t!==r){for(var o=(i=t,a=1+(256/t.length>>0),new Array(a+1).join(i)),s=[],c=0;c<256;c++)s[c]=c;var u=0;for(c=0;c<256;c++){var l=s[c];u=(u+l+o.charCodeAt(c))%256,s[c]=s[u],s[u]=l}r=t,n=s}else s=n;var h=e.length,f=0,d=0,p="";for(c=0;c€/\f©þdSiz";var a=(e+this.padding).substr(0,32),o=(r+this.padding).substr(0,32);this.O=this.processOwnerPassword(a,o),this.P=-(1+(255^i)),this.encryptionKey=A(a+this.O+this.lsbFirstWord(this.P)+this.hexToBytes(n)).substr(0,5),this.U=_(this.encryptionKey,this.padding)}function F(t){if(/[^\u0000-\u00ff]/.test(t))throw new Error("Invalid PDF Name Object: "+t+", Only accept ASCII characters.");for(var e="",r=t.length,n=0;n126)e+="#"+("0"+i.toString(16)).slice(-2);else e+=t[n]}return e}function I(t){if("object"!==e(t))throw new Error("Invalid Context passed to initialize PubSub (jsPDF-module)");var n={};this.subscribe=function(t,e,r){if(r=r||!1,"string"!=typeof t||"function"!=typeof e||"boolean"!=typeof r)throw new Error("Invalid arguments passed to PubSub.subscribe (jsPDF-module)");n.hasOwnProperty(t)||(n[t]={});var i=Math.random().toString(35);return n[t][i]=[e,!!r],i},this.unsubscribe=function(t){for(var e in n)if(n[e][t])return delete n[e][t],0===Object.keys(n[e]).length&&delete n[e],!0;return!1},this.publish=function(e){if(n.hasOwnProperty(e)){var a=Array.prototype.slice.call(arguments,1),o=[];for(var s in n[e]){var c=n[e][s];try{c[0].apply(t,a)}catch(t){r.console&&i.error("jsPDF PubSub Error",t.message,t)}c[1]&&o.push(s)}o.length&&o.forEach(this.unsubscribe)}},this.getTopics=function(){return n}}function C(t){if(!(this instanceof C))return new C(t);var e="opacity,stroke-opacity".split(",");for(var r in t)t.hasOwnProperty(r)&&e.indexOf(r)>=0&&(this[r]=t[r]);this.id="",this.objectNumber=-1}function j(t,e){this.gState=t,this.matrix=e,this.id="",this.objectNumber=-1}function O(t,e,r,n,i){if(!(this instanceof O))return new O(t,e,r,n,i);this.type="axial"===t?2:3,this.coords=e,this.colors=r,j.call(this,n,i)}function B(t,e,r,n,i){if(!(this instanceof B))return new B(t,e,r,n,i);this.boundingBox=t,this.xStep=e,this.yStep=r,this.stream="",this.cloneIndex=0,j.call(this,n,i)}function M(t){var n,a="string"==typeof arguments[0]?arguments[0]:"p",o=arguments[1],s=arguments[2],c=arguments[3],f=[],d=1,p=16,g="S",m=null;"object"===e(t=t||{})&&(a=t.orientation,o=t.unit||o,s=t.format||s,c=t.compress||t.compressPdf||c,null!==(m=t.encryption||null)&&(m.userPassword=m.userPassword||"",m.ownerPassword=m.ownerPassword||"",m.userPermissions=m.userPermissions||[]),d="number"==typeof t.userUnit?Math.abs(t.userUnit):1,void 0!==t.precision&&(n=t.precision),void 0!==t.floatPrecision&&(p=t.floatPrecision),g=t.defaultPathOperation||"S"),f=t.filters||(!0===c?["FlateEncode"]:f),o=o||"mm",a=(""+(a||"P")).toLowerCase();var v=t.putOnlyUsedFonts||!1,b={},y={internal:{},__private__:{}};y.__private__.PubSub=I;var w="1.3",N=y.__private__.getPdfVersion=function(){return w};y.__private__.setPdfVersion=function(t){w=t};var L={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};y.__private__.getPageFormats=function(){return L};var A=y.__private__.getPageFormat=function(t){return L[t]};s=s||"a4";var x={COMPAT:"compat",ADVANCED:"advanced"},S=x.COMPAT;function _(){this.saveGraphicsState(),ht(new Vt(_t,0,0,-_t,0,Rr()*_t).toString()+" cm"),this.setFontSize(this.getFontSize()/_t),g="n",S=x.ADVANCED}function P(){this.restoreGraphicsState(),g="S",S=x.COMPAT}var j=y.__private__.combineFontStyleAndFontWeight=function(t,e){if("bold"==t&&"normal"==e||"bold"==t&&400==e||"normal"==t&&"italic"==e||"bold"==t&&"italic"==e)throw new Error("Invalid Combination of fontweight and fontstyle");return e&&(t=400==e||"normal"===e?"italic"===t?"italic":"normal":700!=e&&"bold"!==e||"normal"!==t?(700==e?"bold":e)+""+t:"bold"),t};y.advancedAPI=function(t){var e=S===x.COMPAT;return e&&_.call(this),"function"!=typeof t||(t(this),e&&P.call(this)),this},y.compatAPI=function(t){var e=S===x.ADVANCED;return e&&P.call(this),"function"!=typeof t||(t(this),e&&_.call(this)),this},y.isAdvancedAPI=function(){return S===x.ADVANCED};var E,q=function(t){if(S!==x.ADVANCED)throw new Error(t+" is only available in 'advanced' API mode. You need to call advancedAPI() first.")},D=y.roundToPrecision=y.__private__.roundToPrecision=function(t,e){var r=n||e;if(isNaN(t)||isNaN(r))throw new Error("Invalid argument passed to jsPDF.roundToPrecision");return t.toFixed(r).replace(/0+$/,"")};E=y.hpf=y.__private__.hpf="number"==typeof p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,p)}:"smart"===p?function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,t>-1&&t<1?16:5)}:function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.hpf");return D(t,16)};var R=y.f2=y.__private__.f2=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f2");return D(t,2)},T=y.__private__.f3=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.f3");return D(t,3)},U=y.scale=y.__private__.scale=function(t){if(isNaN(t))throw new Error("Invalid argument passed to jsPDF.scale");return S===x.COMPAT?t*_t:S===x.ADVANCED?t:void 0},z=function(t){return S===x.COMPAT?Rr()-t:S===x.ADVANCED?t:void 0},H=function(t){return U(z(t))};y.__private__.setPrecision=y.setPrecision=function(t){"number"==typeof parseInt(t,10)&&(n=parseInt(t,10))};var W,V="00000000000000000000000000000000",G=y.__private__.getFileId=function(){return V},Y=y.__private__.setFileId=function(t){return V=void 0!==t&&/^[a-fA-F0-9]{32}$/.test(t)?t.toUpperCase():V.split("").map((function(){return"ABCDEF0123456789".charAt(Math.floor(16*Math.random()))})).join(""),null!==m&&(Ye=new k(m.userPermissions,m.userPassword,m.ownerPassword,V)),V};y.setFileId=function(t){return Y(t),this},y.getFileId=function(){return G()};var J=y.__private__.convertDateToPDFDate=function(t){var e=t.getTimezoneOffset(),r=e<0?"+":"-",n=Math.floor(Math.abs(e/60)),i=Math.abs(e%60),a=[r,Q(n),"'",Q(i),"'"].join("");return["D:",t.getFullYear(),Q(t.getMonth()+1),Q(t.getDate()),Q(t.getHours()),Q(t.getMinutes()),Q(t.getSeconds()),a].join("")},X=y.__private__.convertPDFDateToDate=function(t){var e=parseInt(t.substr(2,4),10),r=parseInt(t.substr(6,2),10)-1,n=parseInt(t.substr(8,2),10),i=parseInt(t.substr(10,2),10),a=parseInt(t.substr(12,2),10),o=parseInt(t.substr(14,2),10);return new Date(e,r,n,i,a,o,0)},K=y.__private__.setCreationDate=function(t){var e;if(void 0===t&&(t=new Date),t instanceof Date)e=J(t);else{if(!/^D:(20[0-2][0-9]|203[0-7]|19[7-9][0-9])(0[0-9]|1[0-2])([0-2][0-9]|3[0-1])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[1-5][0-9])(0[0-9]|[1-5][0-9])(\+0[0-9]|\+1[0-4]|-0[0-9]|-1[0-1])'(0[0-9]|[1-5][0-9])'?$/.test(t))throw new Error("Invalid argument passed to jsPDF.setCreationDate");e=t}return W=e},Z=y.__private__.getCreationDate=function(t){var e=W;return"jsDate"===t&&(e=X(W)),e};y.setCreationDate=function(t){return K(t),this},y.getCreationDate=function(t){return Z(t)};var $,Q=y.__private__.padd2=function(t){return("0"+parseInt(t)).slice(-2)},tt=y.__private__.padd2Hex=function(t){return("00"+(t=t.toString())).substr(t.length)},et=0,rt=[],nt=[],it=0,at=[],ot=[],st=!1,ct=nt,ut=function(){et=0,it=0,nt=[],rt=[],at=[],Qt=Kt(),te=Kt()};y.__private__.setCustomOutputDestination=function(t){st=!0,ct=t};var lt=function(t){st||(ct=t)};y.__private__.resetCustomOutputDestination=function(){st=!1,ct=nt};var ht=y.__private__.out=function(t){return t=t.toString(),it+=t.length+1,ct.push(t),ct},ft=y.__private__.write=function(t){return ht(1===arguments.length?t.toString():Array.prototype.join.call(arguments," "))},dt=y.__private__.getArrayBuffer=function(t){for(var e=t.length,r=new ArrayBuffer(e),n=new Uint8Array(r);e--;)n[e]=t.charCodeAt(e);return r},pt=[["Helvetica","helvetica","normal","WinAnsiEncoding"],["Helvetica-Bold","helvetica","bold","WinAnsiEncoding"],["Helvetica-Oblique","helvetica","italic","WinAnsiEncoding"],["Helvetica-BoldOblique","helvetica","bolditalic","WinAnsiEncoding"],["Courier","courier","normal","WinAnsiEncoding"],["Courier-Bold","courier","bold","WinAnsiEncoding"],["Courier-Oblique","courier","italic","WinAnsiEncoding"],["Courier-BoldOblique","courier","bolditalic","WinAnsiEncoding"],["Times-Roman","times","normal","WinAnsiEncoding"],["Times-Bold","times","bold","WinAnsiEncoding"],["Times-Italic","times","italic","WinAnsiEncoding"],["Times-BoldItalic","times","bolditalic","WinAnsiEncoding"],["ZapfDingbats","zapfdingbats","normal",null],["Symbol","symbol","normal",null]];y.__private__.getStandardFonts=function(){return pt};var gt=t.fontSize||16;y.__private__.setFontSize=y.setFontSize=function(t){return gt=S===x.ADVANCED?t/_t:t,this};var mt,vt=y.__private__.getFontSize=y.getFontSize=function(){return S===x.COMPAT?gt:gt*_t},bt=t.R2L||!1;y.__private__.setR2L=y.setR2L=function(t){return bt=t,this},y.__private__.getR2L=y.getR2L=function(){return bt};var yt,wt=y.__private__.setZoomMode=function(t){var e=[void 0,null,"fullwidth","fullheight","fullpage","original"];if(/^(?:\d+\.\d*|\d*\.\d+|\d+)%$/.test(t))mt=t;else if(isNaN(t)){if(-1===e.indexOf(t))throw new Error('zoom must be Integer (e.g. 2), a percentage Value (e.g. 300%) or fullwidth, fullheight, fullpage, original. "'+t+'" is not recognized.');mt=t}else mt=parseInt(t,10)};y.__private__.getZoomMode=function(){return mt};var Nt,Lt=y.__private__.setPageMode=function(t){if(-1==[void 0,null,"UseNone","UseOutlines","UseThumbs","FullScreen"].indexOf(t))throw new Error('Page mode must be one of UseNone, UseOutlines, UseThumbs, or FullScreen. "'+t+'" is not recognized.');yt=t};y.__private__.getPageMode=function(){return yt};var At=y.__private__.setLayoutMode=function(t){if(-1==[void 0,null,"continuous","single","twoleft","tworight","two"].indexOf(t))throw new Error('Layout mode must be one of continuous, single, twoleft, tworight. "'+t+'" is not recognized.');Nt=t};y.__private__.getLayoutMode=function(){return Nt},y.__private__.setDisplayMode=y.setDisplayMode=function(t,e,r){return wt(t),At(e),Lt(r),this};var xt={title:"",subject:"",author:"",keywords:"",creator:""};y.__private__.getDocumentProperty=function(t){if(-1===Object.keys(xt).indexOf(t))throw new Error("Invalid argument passed to jsPDF.getDocumentProperty");return xt[t]},y.__private__.getDocumentProperties=function(){return xt},y.__private__.setDocumentProperties=y.setProperties=y.setDocumentProperties=function(t){for(var e in xt)xt.hasOwnProperty(e)&&t[e]&&(xt[e]=t[e]);return this},y.__private__.setDocumentProperty=function(t,e){if(-1===Object.keys(xt).indexOf(t))throw new Error("Invalid arguments passed to jsPDF.setDocumentProperty");return xt[t]=e};var St,_t,Pt,kt,Ft,It={},Ct={},jt=[],Ot={},Bt={},Mt={},Et={},qt=null,Dt=0,Rt=[],Tt=new I(y),Ut=t.hotfixes||[],zt={},Ht={},Wt=[],Vt=function t(e,r,n,i,a,o){if(!(this instanceof t))return new t(e,r,n,i,a,o);isNaN(e)&&(e=1),isNaN(r)&&(r=0),isNaN(n)&&(n=0),isNaN(i)&&(i=1),isNaN(a)&&(a=0),isNaN(o)&&(o=0),this._matrix=[e,r,n,i,a,o]};Object.defineProperty(Vt.prototype,"sx",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Vt.prototype,"shy",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Vt.prototype,"shx",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Vt.prototype,"sy",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Vt.prototype,"tx",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Vt.prototype,"ty",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Vt.prototype,"a",{get:function(){return this._matrix[0]},set:function(t){this._matrix[0]=t}}),Object.defineProperty(Vt.prototype,"b",{get:function(){return this._matrix[1]},set:function(t){this._matrix[1]=t}}),Object.defineProperty(Vt.prototype,"c",{get:function(){return this._matrix[2]},set:function(t){this._matrix[2]=t}}),Object.defineProperty(Vt.prototype,"d",{get:function(){return this._matrix[3]},set:function(t){this._matrix[3]=t}}),Object.defineProperty(Vt.prototype,"e",{get:function(){return this._matrix[4]},set:function(t){this._matrix[4]=t}}),Object.defineProperty(Vt.prototype,"f",{get:function(){return this._matrix[5]},set:function(t){this._matrix[5]=t}}),Object.defineProperty(Vt.prototype,"rotation",{get:function(){return Math.atan2(this.shx,this.sx)}}),Object.defineProperty(Vt.prototype,"scaleX",{get:function(){return this.decompose().scale.sx}}),Object.defineProperty(Vt.prototype,"scaleY",{get:function(){return this.decompose().scale.sy}}),Object.defineProperty(Vt.prototype,"isIdentity",{get:function(){return 1===this.sx&&(0===this.shy&&(0===this.shx&&(1===this.sy&&(0===this.tx&&0===this.ty))))}}),Vt.prototype.join=function(t){return[this.sx,this.shy,this.shx,this.sy,this.tx,this.ty].map(E).join(t)},Vt.prototype.multiply=function(t){var e=t.sx*this.sx+t.shy*this.shx,r=t.sx*this.shy+t.shy*this.sy,n=t.shx*this.sx+t.sy*this.shx,i=t.shx*this.shy+t.sy*this.sy,a=t.tx*this.sx+t.ty*this.shx+this.tx,o=t.tx*this.shy+t.ty*this.sy+this.ty;return new Vt(e,r,n,i,a,o)},Vt.prototype.decompose=function(){var t=this.sx,e=this.shy,r=this.shx,n=this.sy,i=this.tx,a=this.ty,o=Math.sqrt(t*t+e*e),s=(t/=o)*r+(e/=o)*n;r-=t*s,n-=e*s;var c=Math.sqrt(r*r+n*n);return s/=c,t*(n/=c)>16&255,i=u>>8&255,a=255&u}if(void 0===i||void 0===o&&n===i&&i===a)if("string"==typeof n)r=n+" "+s[0];else switch(t.precision){case 2:r=R(n/255)+" "+s[0];break;case 3:default:r=T(n/255)+" "+s[0]}else if(void 0===o||"object"===e(o)){if(o&&!isNaN(o.a)&&0===o.a)return r=["1.","1.","1.",s[1]].join(" ");if("string"==typeof n)r=[n,i,a,s[1]].join(" ");else switch(t.precision){case 2:r=[R(n/255),R(i/255),R(a/255),s[1]].join(" ");break;default:case 3:r=[T(n/255),T(i/255),T(a/255),s[1]].join(" ")}}else if("string"==typeof n)r=[n,i,a,o,s[2]].join(" ");else switch(t.precision){case 2:r=[R(n),R(i),R(a),R(o),s[2]].join(" ");break;case 3:default:r=[T(n),T(i),T(a),T(o),s[2]].join(" ")}return r},ne=y.__private__.getFilters=function(){return f},ie=y.__private__.putStream=function(t){var e=(t=t||{}).data||"",r=t.filters||ne(),n=t.alreadyAppliedFilters||[],i=t.addLength1||!1,a=e.length,o=t.objectId,s=function(t){return t};if(null!==m&&void 0===o)throw new Error("ObjectId must be passed to putStream for file encryption");null!==m&&(s=Ye.encryptor(o,0));var c={};!0===r&&(r=["FlateEncode"]);var u=t.additionalKeyValues||[],l=(c=void 0!==M.API.processDataByFilters?M.API.processDataByFilters(e,r):{data:e,reverseChain:[]}).reverseChain+(Array.isArray(n)?n.join(" "):n.toString());if(0!==c.data.length&&(u.push({key:"Length",value:c.data.length}),!0===i&&u.push({key:"Length1",value:a})),0!=l.length)if(l.split("/").length-1==1)u.push({key:"Filter",value:l});else{u.push({key:"Filter",value:"["+l+"]"});for(var h=0;h>"),0!==c.data.length&&(ht("stream"),ht(s(c.data)),ht("endstream"))},ae=y.__private__.putPage=function(t){var e=t.number,r=t.data,n=t.objId,i=t.contentsObjId;Zt(n,!0),ht("<>"),ht("endobj");var a=r.join("\n");return S===x.ADVANCED&&(a+="\nQ"),Zt(i,!0),ie({data:a,filters:ne(),objectId:i}),ht("endobj"),n},oe=y.__private__.putPages=function(){var t,e,r=[];for(t=1;t<=Dt;t++)Rt[t].objId=Kt(),Rt[t].contentsObjId=Kt();for(t=1;t<=Dt;t++)r.push(ae({number:t,data:ot[t],objId:Rt[t].objId,contentsObjId:Rt[t].contentsObjId,mediaBox:Rt[t].mediaBox,cropBox:Rt[t].cropBox,bleedBox:Rt[t].bleedBox,trimBox:Rt[t].trimBox,artBox:Rt[t].artBox,userUnit:Rt[t].userUnit,rootDictionaryObjId:Qt,resourceDictionaryObjId:te}));Zt(Qt,!0),ht("<>"),ht("endobj"),Tt.publish("postPutPages")},se=function(t){Tt.publish("putFont",{font:t,out:ht,newObject:Xt,putStream:ie}),!0!==t.isAlreadyPutted&&(t.objectNumber=Xt(),ht("<<"),ht("/Type /Font"),ht("/BaseFont /"+F(t.postScriptName)),ht("/Subtype /Type1"),"string"==typeof t.encoding&&ht("/Encoding /"+t.encoding),ht("/FirstChar 32"),ht("/LastChar 255"),ht(">>"),ht("endobj"))},ce=function(){for(var t in It)It.hasOwnProperty(t)&&(!1===v||!0===v&&b.hasOwnProperty(t))&&se(It[t])},ue=function(t){t.objectNumber=Xt();var e=[];e.push({key:"Type",value:"/XObject"}),e.push({key:"Subtype",value:"/Form"}),e.push({key:"BBox",value:"["+[E(t.x),E(t.y),E(t.x+t.width),E(t.y+t.height)].join(" ")+"]"}),e.push({key:"Matrix",value:"["+t.matrix.toString()+"]"});var r=t.pages[1].join("\n");ie({data:r,additionalKeyValues:e,objectId:t.objectNumber}),ht("endobj")},le=function(){for(var t in zt)zt.hasOwnProperty(t)&&ue(zt[t])},he=function(t,e){var r,n=[],i=1/(e-1);for(r=0;r<1;r+=i)n.push(r);if(n.push(1),0!=t[0].offset){var a={offset:0,color:t[0].color};t.unshift(a)}if(1!=t[t.length-1].offset){var o={offset:1,color:t[t.length-1].color};t.push(o)}for(var s="",c=0,u=0;ut[c+1].offset;)c++;var l=t[c].offset,h=(r-l)/(t[c+1].offset-l),f=t[c].color,d=t[c+1].color;s+=tt(Math.round((1-h)*f[0]+h*d[0]).toString(16))+tt(Math.round((1-h)*f[1]+h*d[1]).toString(16))+tt(Math.round((1-h)*f[2]+h*d[2]).toString(16))}return s.trim()},fe=function(t,e){e||(e=21);var r=Xt(),n=he(t.colors,e),i=[];i.push({key:"FunctionType",value:"0"}),i.push({key:"Domain",value:"[0.0 1.0]"}),i.push({key:"Size",value:"["+e+"]"}),i.push({key:"BitsPerSample",value:"8"}),i.push({key:"Range",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),i.push({key:"Decode",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),ie({data:n,additionalKeyValues:i,alreadyAppliedFilters:["/ASCIIHexDecode"],objectId:r}),ht("endobj"),t.objectNumber=Xt(),ht("<< /ShadingType "+t.type),ht("/ColorSpace /DeviceRGB");var a="/Coords ["+E(parseFloat(t.coords[0]))+" "+E(parseFloat(t.coords[1]))+" ";2===t.type?a+=E(parseFloat(t.coords[2]))+" "+E(parseFloat(t.coords[3])):a+=E(parseFloat(t.coords[2]))+" "+E(parseFloat(t.coords[3]))+" "+E(parseFloat(t.coords[4]))+" "+E(parseFloat(t.coords[5])),ht(a+="]"),t.matrix&&ht("/Matrix ["+t.matrix.toString()+"]"),ht("/Function "+r+" 0 R"),ht("/Extend [true true]"),ht(">>"),ht("endobj")},de=function(t,e){var r=Kt(),n=Xt();e.push({resourcesOid:r,objectOid:n}),t.objectNumber=n;var i=[];i.push({key:"Type",value:"/Pattern"}),i.push({key:"PatternType",value:"1"}),i.push({key:"PaintType",value:"1"}),i.push({key:"TilingType",value:"1"}),i.push({key:"BBox",value:"["+t.boundingBox.map(E).join(" ")+"]"}),i.push({key:"XStep",value:E(t.xStep)}),i.push({key:"YStep",value:E(t.yStep)}),i.push({key:"Resources",value:r+" 0 R"}),t.matrix&&i.push({key:"Matrix",value:"["+t.matrix.toString()+"]"}),ie({data:t.stream,additionalKeyValues:i,objectId:t.objectNumber}),ht("endobj")},pe=function(t){var e;for(e in Ot)Ot.hasOwnProperty(e)&&(Ot[e]instanceof O?fe(Ot[e]):Ot[e]instanceof B&&de(Ot[e],t))},ge=function(t){for(var e in t.objectNumber=Xt(),ht("<<"),t)switch(e){case"opacity":ht("/ca "+R(t[e]));break;case"stroke-opacity":ht("/CA "+R(t[e]))}ht(">>"),ht("endobj")},me=function(){var t;for(t in Mt)Mt.hasOwnProperty(t)&&ge(Mt[t])},ve=function(){for(var t in ht("/XObject <<"),zt)zt.hasOwnProperty(t)&&zt[t].objectNumber>=0&&ht("/"+t+" "+zt[t].objectNumber+" 0 R");Tt.publish("putXobjectDict"),ht(">>")},be=function(){Ye.oid=Xt(),ht("<<"),ht("/Filter /Standard"),ht("/V "+Ye.v),ht("/R "+Ye.r),ht("/U <"+Ye.toHexString(Ye.U)+">"),ht("/O <"+Ye.toHexString(Ye.O)+">"),ht("/P "+Ye.P),ht(">>"),ht("endobj")},ye=function(){for(var t in ht("/Font <<"),It)It.hasOwnProperty(t)&&(!1===v||!0===v&&b.hasOwnProperty(t))&&ht("/"+t+" "+It[t].objectNumber+" 0 R");ht(">>")},we=function(){if(Object.keys(Ot).length>0){for(var t in ht("/Shading <<"),Ot)Ot.hasOwnProperty(t)&&Ot[t]instanceof O&&Ot[t].objectNumber>=0&&ht("/"+t+" "+Ot[t].objectNumber+" 0 R");Tt.publish("putShadingPatternDict"),ht(">>")}},Ne=function(t){if(Object.keys(Ot).length>0){for(var e in ht("/Pattern <<"),Ot)Ot.hasOwnProperty(e)&&Ot[e]instanceof y.TilingPattern&&Ot[e].objectNumber>=0&&Ot[e].objectNumber>")}},Le=function(){if(Object.keys(Mt).length>0){var t;for(t in ht("/ExtGState <<"),Mt)Mt.hasOwnProperty(t)&&Mt[t].objectNumber>=0&&ht("/"+t+" "+Mt[t].objectNumber+" 0 R");Tt.publish("putGStateDict"),ht(">>")}},Ae=function(t){Zt(t.resourcesOid,!0),ht("<<"),ht("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"),ye(),we(),Ne(t.objectOid),Le(),ve(),ht(">>"),ht("endobj")},xe=function(){var t=[];ce(),me(),le(),pe(t),Tt.publish("putResources"),t.forEach(Ae),Ae({resourcesOid:te,objectOid:Number.MAX_SAFE_INTEGER}),Tt.publish("postPutResources")},Se=function(){Tt.publish("putAdditionalObjects");for(var t=0;t>8&&(c=!0);t=s.join("")}for(r=t.length;void 0===c&&0!==r;)t.charCodeAt(r-1)>>8&&(c=!0),r--;if(!c)return t;for(s=e.noBOM?[]:[254,255],r=0,n=t.length;r>8)>>8)throw new Error("Character at position "+r+" of string '"+t+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");s.push(l),s.push(u-(l<<8))}return String.fromCharCode.apply(void 0,s)},Ce=y.__private__.pdfEscape=y.pdfEscape=function(t,e){return Ie(t,e).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},je=y.__private__.beginPage=function(t){ot[++Dt]=[],Rt[Dt]={objId:0,contentsObjId:0,userUnit:Number(d),artBox:null,bleedBox:null,cropBox:null,trimBox:null,mediaBox:{bottomLeftX:0,bottomLeftY:0,topRightX:Number(t[0]),topRightY:Number(t[1])}},Me(Dt),lt(ot[$])},Oe=function(t,e){var r,n,o;switch(a=e||a,"string"==typeof t&&(r=A(t.toLowerCase()),Array.isArray(r)&&(n=r[0],o=r[1])),Array.isArray(t)&&(n=t[0]*_t,o=t[1]*_t),isNaN(n)&&(n=s[0],o=s[1]),(n>14400||o>14400)&&(i.warn("A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"),n=Math.min(14400,n),o=Math.min(14400,o)),s=[n,o],a.substr(0,1)){case"l":o>n&&(s=[o,n]);break;case"p":n>o&&(s=[o,n])}je(s),pr(fr),ht(Lr),0!==kr&&ht(kr+" J"),0!==Fr&&ht(Fr+" j"),Tt.publish("addPage",{pageNumber:Dt})},Be=function(t){t>0&&t<=Dt&&(ot.splice(t,1),Rt.splice(t,1),Dt--,$>Dt&&($=Dt),this.setPage($))},Me=function(t){t>0&&t<=Dt&&($=t)},Ee=y.__private__.getNumberOfPages=y.getNumberOfPages=function(){return ot.length-1},qe=function(t,e,r){var n,a=void 0;return r=r||{},t=void 0!==t?t:It[St].fontName,e=void 0!==e?e:It[St].fontStyle,n=t.toLowerCase(),void 0!==Ct[n]&&void 0!==Ct[n][e]?a=Ct[n][e]:void 0!==Ct[t]&&void 0!==Ct[t][e]?a=Ct[t][e]:!1===r.disableWarning&&i.warn("Unable to look up font label for font '"+t+"', '"+e+"'. Refer to getFontList() for available fonts."),a||r.noFallback||null==(a=Ct.times[e])&&(a=Ct.times.normal),a},De=y.__private__.putInfo=function(){var t=Xt(),e=function(t){return t};for(var r in null!==m&&(e=Ye.encryptor(t,0)),ht("<<"),ht("/Producer ("+Ce(e("jsPDF "+M.version))+")"),xt)xt.hasOwnProperty(r)&&xt[r]&&ht("/"+r.substr(0,1).toUpperCase()+r.substr(1)+" ("+Ce(e(xt[r]))+")");ht("/CreationDate ("+Ce(e(W))+")"),ht(">>"),ht("endobj")},Re=y.__private__.putCatalog=function(t){var e=(t=t||{}).rootDictionaryObjId||Qt;switch(Xt(),ht("<<"),ht("/Type /Catalog"),ht("/Pages "+e+" 0 R"),mt||(mt="fullwidth"),mt){case"fullwidth":ht("/OpenAction [3 0 R /FitH null]");break;case"fullheight":ht("/OpenAction [3 0 R /FitV null]");break;case"fullpage":ht("/OpenAction [3 0 R /Fit]");break;case"original":ht("/OpenAction [3 0 R /XYZ null null 1]");break;default:var r=""+mt;"%"===r.substr(r.length-1)&&(mt=parseInt(mt)/100),"number"==typeof mt&&ht("/OpenAction [3 0 R /XYZ null null "+R(mt)+"]")}switch(Nt||(Nt="continuous"),Nt){case"continuous":ht("/PageLayout /OneColumn");break;case"single":ht("/PageLayout /SinglePage");break;case"two":case"twoleft":ht("/PageLayout /TwoColumnLeft");break;case"tworight":ht("/PageLayout /TwoColumnRight")}yt&&ht("/PageMode /"+yt),Tt.publish("putCatalog"),ht(">>"),ht("endobj")},Te=y.__private__.putTrailer=function(){ht("trailer"),ht("<<"),ht("/Size "+(et+1)),ht("/Root "+et+" 0 R"),ht("/Info "+(et-1)+" 0 R"),null!==m&&ht("/Encrypt "+Ye.oid+" 0 R"),ht("/ID [ <"+V+"> <"+V+"> ]"),ht(">>")},Ue=y.__private__.putHeader=function(){ht("%PDF-"+w),ht("%ºß¬à")},ze=y.__private__.putXRef=function(){var t="0000000000";ht("xref"),ht("0 "+(et+1)),ht("0000000000 65535 f ");for(var e=1;e<=et;e++){"function"==typeof rt[e]?ht((t+rt[e]()).slice(-10)+" 00000 n "):void 0!==rt[e]?ht((t+rt[e]).slice(-10)+" 00000 n "):ht("0000000000 00000 n ")}},He=y.__private__.buildDocument=function(){ut(),lt(nt),Tt.publish("buildDocument"),Ue(),oe(),Se(),xe(),null!==m&&be(),De(),Re();var t=it;return ze(),Te(),ht("startxref"),ht(""+t),ht("%%EOF"),lt(ot[$]),nt.join("\n")},We=y.__private__.getBlob=function(t){return new Blob([dt(t)],{type:"application/pdf"})},Ve=y.output=y.__private__.output=Fe((function(t,e){switch("string"==typeof(e=e||{})?e={filename:e}:e.filename=e.filename||"generated.pdf",t){case void 0:return He();case"save":y.save(e.filename);break;case"arraybuffer":return dt(He());case"blob":return We(He());case"bloburi":case"bloburl":if(void 0!==r.URL&&"function"==typeof r.URL.createObjectURL)return r.URL&&r.URL.createObjectURL(We(He()))||void 0;i.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");break;case"datauristring":case"dataurlstring":var n="",a=He();try{n=u(a)}catch(t){n=u(unescape(encodeURIComponent(a)))}return"data:application/pdf;filename="+e.filename+";base64,"+n;case"pdfobjectnewwindow":if("[object Window]"===Object.prototype.toString.call(r)){var o="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",s=' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';e.pdfObjectUrl&&(o=e.pdfObjectUrl,s="");var c=' + @@ -381,6 +500,7 @@ Local Bench
+ Docs Connecting…
-
+

Local LLM benchmark
& intelligence catalog

-

Measure throughput for the models you run locally via Ollama, and compare their skills at various tasks.

+

Measure throughput for the models you run locally via Ollama, and compare their skills at various tasks. Run Ollama with ollama serve in a terminal application.

Catalog models
Curated Companion catalog
@@ -404,7 +524,7 @@

Local LLM benchmark
& intelligence catalog

-
+
@@ -426,14 +546,11 @@

Run a benchmark

-
- - -
-
-
Current prompt
-
Loading…
-
+ + + + +
@@ -451,12 +568,17 @@

Run a benchmark

-
+

Model intelligence

- Artificial Analysis Intelligence Index +
+ Artificial Analysis Intelligence Index + +
-
+

Loading curated catalog…

@@ -496,7 +618,10 @@

Throughput comparison

Detailed results

- +
+ + +
@@ -517,12 +642,110 @@

Detailed results

+ + + + + + +
+
+ +
+ ← Back to dashboard + +
+

Local Bench

+

Local Bench is a companion app that benchmarks local language models on your own hardware. It runs a set of prompts through models served by Ollama, measures how each one performs, and saves the results so you can compare them over time.

+

Everything runs locally. Your prompts and the model responses stay on your machine. Use the tabs on the left to learn what Local Bench shows, the kinds of prompts it runs, and how it helps you plan multi-agent systems.

+
+ + + + + + + + +
+
- Local-Bench — a Companion Intelligence app for benchmarking local LLMs. - Private & Confidential — Property of Lifescope Inc. + Local-Bench, a Companion Intelligence app for benchmarking local LLMs. + + Ecosystem + Discord + + Licensed under the Companion Intelligence Commercial Standard License (CI-Commercial-1.0).
@@ -532,6 +755,9 @@

Detailed results

let availableModels = []; let modelIndexByName = {}; let lastResults = []; + let lastRun = []; + let latestSpecs = null; + let intelligenceAutoCollapsed = false; let intelligenceMeta = { source: 'Artificial Analysis Intelligence Index', url: 'https://artificialanalysis.ai/', asOf: '' }; function escapeHtml(value) { @@ -575,11 +801,41 @@

Detailed results

function renderPromptLibrary(prompts) { document.getElementById('promptLibrary').innerHTML = prompts.map(prompt => ` -
+
+
${escapeHtml(prompt.category)}${escapeHtml(prompt.type)}

${escapeHtml(prompt.name)}

${escapeHtml(prompt.description)}

+
`).join(''); + markSelectedPromptCard(document.getElementById('promptSelect').value); + } + + // Reveal/hide the full prompt text inside a card without selecting it. + function togglePromptCard(btn, event) { + event.stopPropagation(); + const card = btn.closest('.prompt-card'); + if (!card) return; + const text = card.querySelector('.prompt-card-text'); + const expanded = card.classList.toggle('expanded'); + if (text) text.hidden = !expanded; + btn.setAttribute('aria-expanded', String(expanded)); + btn.setAttribute('aria-label', expanded ? 'Hide prompt' : 'Show prompt'); + } + + // Selecting a card loads its prompt into the editable box (in sync with the dropdown). + function selectPromptCard(id) { + const promptSelect = document.getElementById('promptSelect'); + promptSelect.value = id; + handlePromptChange(); + } + + function markSelectedPromptCard(id) { + document.querySelectorAll('#promptLibrary .prompt-card').forEach(card => { + card.classList.toggle('selected', card.dataset.id === id); + }); } function updateOllamaStatus(state) { @@ -590,13 +846,28 @@

${escapeHtml(prompt.name)}

else { dot.className = 'dot'; label.textContent = 'Connecting…'; } } + // Click an inline command chip (e.g. `ollama serve`) to copy it to the clipboard. + document.addEventListener('click', (event) => { + const chip = event.target.closest('code.cmd'); + if (!chip || chip.classList.contains('copied')) return; + const command = chip.textContent; + const showCopied = () => { + chip.classList.add('copied'); + chip.textContent = 'Copied!'; + setTimeout(() => { chip.textContent = command; chip.classList.remove('copied'); }, 1100); + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(command).then(showCopied).catch(() => {}); + } + }); + function updateModelCatalogSummary(models, options = {}) { const summary = document.getElementById('modelCatalogSummary'); const installedCount = models.filter(m => m.installed).length; const supportedCount = models.filter(m => m.supported).length; const customInstalledCount = models.filter(m => m.installed && !m.supported).length; const note = options.ollamaUnavailable - ? 'Ollama is currently unavailable — showing the curated catalog only. Start Ollama to enable benchmarking.' + ? 'Ollama is currently unavailable — showing the curated catalog only. Open a terminal application and run ollama serve to start it, then refresh to enable benchmarking.' : 'Disabled cards are in the curated catalog but not installed locally.'; summary.innerHTML = ` ${installedCount} installed @@ -676,33 +947,27 @@

${escapeHtml(prompt.name)}

function handlePromptChange() { const promptSelect = document.getElementById('promptSelect'); - const customContainer = document.getElementById('customPromptContainer'); const descriptionDiv = document.getElementById('promptDescription'); + const activePrompt = document.getElementById('activePrompt'); const selectedValue = promptSelect.value; if (selectedValue === 'custom') { - customContainer.classList.add('active'); - descriptionDiv.textContent = 'Enter your own custom prompt for benchmarking.'; - updatePromptPreview(); + descriptionDiv.textContent = 'Custom prompt — type anything you want to benchmark below.'; + activePrompt.value = ''; + activePrompt.focus(); } else { - customContainer.classList.remove('active'); const selectedPrompt = availablePrompts.find(p => p.id === selectedValue); if (selectedPrompt) { descriptionDiv.textContent = `${selectedPrompt.category} • ${selectedPrompt.type} — ${selectedPrompt.description}`; - document.getElementById('promptPreviewText').textContent = selectedPrompt.prompt; + activePrompt.value = selectedPrompt.prompt; } } - } - - function updatePromptPreview() { - const customInput = document.getElementById('customPromptInput'); - const previewText = document.getElementById('promptPreviewText'); - previewText.textContent = customInput.value.trim() || '(Enter a custom prompt above)'; + markSelectedPromptCard(selectedValue); } function getPromptConfig() { - const selectedValue = document.getElementById('promptSelect').value; - if (selectedValue === 'custom') return { customPrompt: document.getElementById('customPromptInput').value.trim() }; - return { promptId: selectedValue }; + // The editable box is the single source of truth — base prompts are pre-filled + // there and may be edited, so always send its text as the prompt to run. + return { customPrompt: document.getElementById('activePrompt').value.trim() }; } async function loadSystemSpecs() { @@ -717,6 +982,7 @@

${escapeHtml(prompt.name)}

} function displaySystemSpecs(specs) { + latestSpecs = specs || null; const el = document.getElementById('systemSpecs'); if (!specs) { el.className = 'note'; @@ -787,6 +1053,74 @@

${escapeHtml(prompt.name)}

${escapeHtml(new Date(result.timestamp).toLocaleString())} ${result.status}`; }); + const exportBtn = document.getElementById('exportPdfBtn'); + if (exportBtn) exportBtn.hidden = results.length === 0; + // Once results are ready, collapse the Model intelligence section to keep + // focus on the run output. Only auto-collapse once so manual toggles stick. + if (results.length && !intelligenceAutoCollapsed) { + setIntelligenceCollapsed(true); + intelligenceAutoCollapsed = true; + } + } + + function setIntelligenceCollapsed(collapsed) { + const section = document.getElementById('intelligenceSection'); + const toggle = document.getElementById('intelligenceToggle'); + if (!section || !toggle) return; + section.classList.toggle('collapsed', collapsed); + toggle.setAttribute('aria-expanded', String(!collapsed)); + toggle.setAttribute('aria-label', collapsed ? 'Expand section' : 'Collapse section'); + } + + function toggleIntelligence() { + const section = document.getElementById('intelligenceSection'); + if (section) setIntelligenceCollapsed(!section.classList.contains('collapsed')); + } + + // ---- Two-at-a-time response comparison (from the last run) ---- + function renderResponseComparison() { + const section = document.getElementById('responseSection'); + if (!section) return; + const hasResponses = lastRun.some(r => r && (r.response || !r.success)); + if (!lastRun.length || !hasResponses) { section.hidden = true; return; } + section.hidden = false; + + const promptText = (lastRun.find(r => r && r.prompt) || {}).prompt || ''; + document.getElementById('responsePrompt').textContent = promptText || '(prompt unavailable)'; + + const left = document.getElementById('compareLeft'); + const right = document.getElementById('compareRight'); + const options = lastRun.map((r, i) => + ``).join(''); + const n = lastRun.length; + const clamp = (v, fallback) => { + const i = parseInt(v, 10); + return (Number.isInteger(i) && i >= 0 && i < n) ? String(i) : fallback; + }; + const prevLeft = left.value, prevRight = right.value; + left.innerHTML = options; + right.innerHTML = options; + left.value = clamp(prevLeft, '0'); + right.value = clamp(prevRight, String(n > 1 ? 1 : 0)); + renderCompareSides(); + } + + function renderCompareSides() { + const left = document.getElementById('compareLeft'); + const right = document.getElementById('compareRight'); + fillResponseBox('responseLeft', lastRun[parseInt(left.value, 10)]); + fillResponseBox('responseRight', lastRun[parseInt(right.value, 10)]); + } + + function fillResponseBox(boxId, result) { + const box = document.getElementById(boxId); + if (!box) return; + if (!result) { box.textContent = ''; return; } + if (!result.success) { + box.innerHTML = `No response — run failed${result.error ? ': ' + escapeHtml(result.error) : '.'}`; + return; + } + box.textContent = (result.response || '').trim() || '(empty response)'; } function displayChart(results) { @@ -869,11 +1203,215 @@

${escapeHtml(prompt.name)}

Fastest model
${escapeHtml(fastest.model)}
`; } + // Trim text with an ellipsis so it fits within maxW at the current font/size. + function pdfClip(doc, text, maxW) { + text = String(text ?? ''); + if (doc.getTextWidth(text) <= maxW) return text; + let t = text; + while (t.length > 1 && doc.getTextWidth(t + '…') > maxW) t = t.slice(0, -1); + return t + '…'; + } + + // Build and download a branded PDF report of the current results. + function exportResultsPdf() { + if (!lastResults.length) { showError('Run a benchmark first — there are no results to export yet.'); return; } + const jsPDFCtor = window.jspdf && window.jspdf.jsPDF; + if (!jsPDFCtor) { showError('PDF library failed to load — refresh the page and try again.'); return; } + + const doc = new jsPDFCtor({ unit: 'pt', format: 'a4' }); + const PAGE_W = doc.internal.pageSize.getWidth(); + const PAGE_H = doc.internal.pageSize.getHeight(); + const MARGIN = 40; + const CONTENT_W = PAGE_W - MARGIN * 2; + + const TEAL = [15, 111, 120], TEAL_DK = [9, 94, 102]; + const INK = [20, 24, 26], MUTED = [110, 124, 130]; + const GREEN = [12, 122, 67], RED = [180, 35, 24], ZEBRA = [243, 247, 248]; + + const now = new Date(); + const pad = n => String(n).padStart(2, '0'); + + // ---- Header band (mirrors the tool's top bar) ---- + const HEADER_H = 66; + doc.setFillColor(TEAL[0], TEAL[1], TEAL[2]); + doc.rect(0, 0, PAGE_W, HEADER_H, 'F'); + doc.setTextColor(255, 255, 255); + doc.setFont('helvetica', 'bold'); doc.setFontSize(21); + doc.text('LOCAL-BENCH', MARGIN, 32); + doc.setFont('helvetica', 'normal'); doc.setFontSize(10); + doc.setTextColor(214, 234, 236); + doc.text('Companion Intelligence · Local LLM benchmark report', MARGIN, 50); + doc.setFontSize(9); + doc.text('Generated ' + now.toLocaleString(), PAGE_W - MARGIN, 32, { align: 'right' }); + + let y = HEADER_H + 28; + + // ---- System summary ---- + doc.setTextColor(INK[0], INK[1], INK[2]); + doc.setFont('helvetica', 'bold'); doc.setFontSize(11); + doc.text('System', MARGIN, y); y += 15; + doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5); + doc.setTextColor(MUTED[0], MUTED[1], MUTED[2]); + const s = latestSpecs; + const sysLines = []; + if (s) { + if (s.serverName) sysLines.push('Server: ' + s.serverName); + if (s.cpuModel) sysLines.push('CPU: ' + s.cpuModel + (s.cpuCores ? ' (' + s.cpuCores + ' cores / ' + s.cpuThreads + ' threads)' : '')); + if (s.totalMemoryGB) sysLines.push('Memory: ' + s.totalMemoryGB + ' GB'); + if (s.osType) sysLines.push('OS: ' + s.osType + ' ' + (s.osVersion || '')); + const gpu = (s.gpus || []).map(g => g.model).filter(Boolean).join(', '); + if (gpu) sysLines.push('GPU: ' + gpu); + } + if (!sysLines.length) sysLines.push('System specifications were not captured for this run.'); + sysLines.forEach(line => { + doc.splitTextToSize(line, CONTENT_W).forEach(w => { doc.text(w, MARGIN, y); y += 13; }); + }); + y += 12; + + // ---- Summary stat boxes ---- + const ok = lastResults.filter(r => r.success && r.tokensPerSecond > 0); + const avg = ok.length ? ok.reduce((a, r) => a + r.tokensPerSecond, 0) / ok.length : 0; + const fastest = ok.length ? ok.reduce((m, r) => r.tokensPerSecond > m.tokensPerSecond ? r : m) : null; + const boxes = [ + ['Runs recorded', String(lastResults.length)], + ['Successful', String(ok.length)], + ['Average speed', avg.toFixed(2) + ' t/s'] + ]; + const bw = (CONTENT_W - 12 * 2) / 3, bh = 46; + boxes.forEach((b, i) => { + const bx = MARGIN + i * (bw + 12); + doc.setDrawColor(225, 230, 232); doc.setFillColor(249, 251, 251); + doc.roundedRect(bx, y, bw, bh, 5, 5, 'FD'); + doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.setTextColor(MUTED[0], MUTED[1], MUTED[2]); + doc.text(b[0].toUpperCase(), bx + 10, y + 17); + doc.setFont('helvetica', 'bold'); doc.setFontSize(15); doc.setTextColor(INK[0], INK[1], INK[2]); + doc.text(b[1], bx + 10, y + 37); + }); + y += bh + 16; + if (fastest) { + doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(TEAL_DK[0], TEAL_DK[1], TEAL_DK[2]); + doc.text(pdfClip(doc, 'Top performer: ' + fastest.model + ' — ' + fastest.tokensPerSecond.toFixed(2) + ' tokens/s', CONTENT_W), MARGIN, y); + y += 22; + } + + // ---- Results table ---- + const cols = [ + { key: 'model', label: 'Model', w: 135, align: 'left' }, + { key: 'iq', label: 'IQ', w: 32, align: 'center' }, + { key: 'tps', label: 'Tokens/s', w: 60, align: 'right' }, + { key: 'tot', label: 'Total tokens', w: 66, align: 'right' }, + { key: 'dur', label: 'Duration', w: 58, align: 'right' }, + { key: 'ts', label: 'Timestamp', w: 110, align: 'left' }, + { key: 'st', label: 'Status', w: 54, align: 'left' } + ]; + let cx = MARGIN; cols.forEach(c => { c.x = cx; cx += c.w; }); + const ROW_H = 18, PAD = 6; + const cellX = c => c.align === 'right' ? c.x + c.w - PAD : c.align === 'center' ? c.x + c.w / 2 : c.x + PAD; + const drawHeaderRow = yy => { + doc.setFillColor(TEAL_DK[0], TEAL_DK[1], TEAL_DK[2]); + doc.rect(MARGIN, yy, CONTENT_W, ROW_H, 'F'); + doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(8.5); + cols.forEach(c => doc.text(c.label, cellX(c), yy + 12, { align: c.align })); + return yy + ROW_H; + }; + + doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.setTextColor(INK[0], INK[1], INK[2]); + doc.text('Detailed results', MARGIN, y); y += 8; + y = drawHeaderRow(y); + + lastResults.forEach((r, i) => { + if (y + ROW_H > PAGE_H - 46) { doc.addPage(); y = MARGIN + 10; y = drawHeaderRow(y); } + if (i % 2 === 1) { doc.setFillColor(ZEBRA[0], ZEBRA[1], ZEBRA[2]); doc.rect(MARGIN, y, CONTENT_W, ROW_H, 'F'); } + const cat = modelIndexByName[r.model]; + let ts; try { ts = new Date(r.timestamp).toLocaleString(); } catch (e) { ts = String(r.timestamp || ''); } + const values = { + model: r.model, + iq: (cat && cat.intelligenceIndex != null) ? String(cat.intelligenceIndex) : '—', + tps: r.tokensPerSecond.toFixed(2), + tot: String(r.totalTokens), + dur: r.duration.toFixed(2) + ' s', + ts: ts, + st: r.success ? 'Success' : 'Failed' + }; + doc.setFontSize(8.5); + cols.forEach(c => { + doc.setFont('helvetica', c.key === 'model' ? 'bold' : 'normal'); + if (c.key === 'st') doc.setTextColor(...(r.success ? GREEN : RED)); + else doc.setTextColor(INK[0], INK[1], INK[2]); + doc.text(pdfClip(doc, values[c.key], c.w - PAD * 2), cellX(c), y + 12, { align: c.align }); + }); + y += ROW_H; + }); + + // ---- Model responses (last pages, from the last run) ---- + const responded = (lastRun || []).filter(r => r && (r.response || !r.success)); + if (responded.length) { + doc.addPage(); y = MARGIN + 10; + doc.setFont('helvetica', 'bold'); doc.setFontSize(13); doc.setTextColor(INK[0], INK[1], INK[2]); + doc.text('Model responses', MARGIN, y); y += 18; + + const promptText = (lastRun.find(r => r && r.prompt) || {}).prompt || ''; + if (promptText) { + doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(MUTED[0], MUTED[1], MUTED[2]); + doc.text('PROMPT', MARGIN, y); y += 13; + doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5); doc.setTextColor(INK[0], INK[1], INK[2]); + doc.splitTextToSize(promptText, CONTENT_W).forEach(line => { + if (y > PAGE_H - 50) { doc.addPage(); y = MARGIN + 10; } + doc.text(line, MARGIN, y); y += 13; + }); + y += 10; + } + + const MAX_CHARS = 6000; + responded.forEach(r => { + if (y > PAGE_H - 80) { doc.addPage(); y = MARGIN + 10; } + doc.setFillColor(TEAL_DK[0], TEAL_DK[1], TEAL_DK[2]); + doc.rect(MARGIN, y, CONTENT_W, 20, 'F'); + doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); + doc.text(pdfClip(doc, r.model, CONTENT_W - 150), MARGIN + 8, y + 13); + doc.setFont('helvetica', 'normal'); doc.setFontSize(8.5); + doc.text(r.success ? (r.tokensPerSecond.toFixed(2) + ' t/s · ' + r.totalTokens + ' tok') : 'Failed', + MARGIN + CONTENT_W - 8, y + 13, { align: 'right' }); + y += 28; + + doc.setTextColor(INK[0], INK[1], INK[2]); doc.setFont('helvetica', 'normal'); doc.setFontSize(9); + let body = r.success ? (r.response || '').trim() : ('No response — run failed' + (r.error ? ': ' + r.error : '.')); + if (!body) body = '(empty response)'; + let truncated = false; + if (body.length > MAX_CHARS) { body = body.slice(0, MAX_CHARS); truncated = true; } + doc.splitTextToSize(body, CONTENT_W).forEach(line => { + if (y > PAGE_H - 46) { doc.addPage(); y = MARGIN + 10; } + doc.text(line, MARGIN, y); y += 12.5; + }); + if (truncated) { + if (y > PAGE_H - 46) { doc.addPage(); y = MARGIN + 10; } + doc.setFont('helvetica', 'italic'); doc.setFontSize(8); doc.setTextColor(MUTED[0], MUTED[1], MUTED[2]); + doc.text('… response truncated for the report.', MARGIN, y); y += 14; + } + y += 14; + }); + } + + // ---- Footer on every page ---- + const pages = doc.getNumberOfPages(); + for (let p = 1; p <= pages; p++) { + doc.setPage(p); + doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.setTextColor(150, 160, 164); + doc.text('Private & Confidential — Property of Lifescope Inc.', MARGIN, PAGE_H - 22); + doc.text('Page ' + p + ' of ' + pages, PAGE_W - MARGIN, PAGE_H - 22, { align: 'right' }); + } + + const fname = `local-bench-results-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.pdf`; + doc.save(fname); + } + function showError(message) { document.getElementById('errorContainer').innerHTML = `
Error: ${escapeHtml(message)}
`; } function showEmptyState() { document.getElementById('errorContainer').innerHTML = `
Welcome to Local-Bench. No benchmarks have run yet — select one or more installed models above, pick a prompt, and run a benchmark. Results, charts, and system specs appear here automatically.
`; + const exportBtn = document.getElementById('exportPdfBtn'); + if (exportBtn) exportBtn.hidden = true; } function clearError() { document.getElementById('errorContainer').innerHTML = ''; } @@ -930,7 +1468,7 @@

${escapeHtml(prompt.name)}

const selectedModels = Array.from(document.querySelectorAll('.model-checkbox-input:checked')).map(cb => cb.value); if (selectedModels.length === 0) { showBenchmarkAlert('Please select at least one installed model to benchmark.', 'error'); return; } const promptConfig = getPromptConfig(); - if ('customPrompt' in promptConfig && promptConfig.customPrompt === '') { showBenchmarkAlert('Please enter a custom prompt or select a predefined test.', 'error'); return; } + if ('customPrompt' in promptConfig && promptConfig.customPrompt === '') { showBenchmarkAlert('Please enter or select a prompt to run.', 'error'); return; } const runBtn = document.getElementById('runBenchmarkBtn'); runBtn.disabled = true; runBtn.textContent = 'Running…'; @@ -951,11 +1489,13 @@

${escapeHtml(prompt.name)}

throw new Error(errorMessage); } const result = await response.json(); + lastRun = Array.isArray(result.results) ? result.results : []; progressFill.style.width = '100%'; progressStatus.textContent = 'Benchmark completed.'; const successCount = result.results.filter(r => r.success).length; const failedCount = result.results.length - successCount; showBenchmarkAlert(`Benchmark completed — ${successCount} successful${failedCount > 0 ? `, ${failedCount} failed` : ''}.`, 'success'); + renderResponseComparison(); setTimeout(() => { loadResults(); loadSystemSpecs(); }, 800); } catch (error) { progressFill.style.width = '100%'; @@ -986,7 +1526,32 @@

${escapeHtml(prompt.name)}

}); } catch (e) { /* matchMedia unavailable — stay on initial theme */ } + // ---- In-app docs routing (#docs) ---- + function showDocs(show) { + const docs = document.getElementById('docsView'); + const hero = document.getElementById('heroWrap'); + const dash = document.getElementById('dashboardMain'); + const nav = document.getElementById('docsNav'); + if (!docs || !hero || !dash) return; + docs.hidden = !show; + hero.hidden = show; + dash.hidden = show; + if (nav) nav.classList.toggle('active', show); + if (show) window.scrollTo(0, 0); + } + function applyDocsRoute() { showDocs(location.hash === '#docs'); } + window.addEventListener('hashchange', applyDocsRoute); + + // Switch the active documentation topic (one tab per topic, CI-Import-Tools style). + function selectDocTopic(topic) { + document.querySelectorAll('#docsView .docs-topic').forEach(s => { s.hidden = s.id !== 'topic-' + topic; }); + document.querySelectorAll('#docsView .docs-tab').forEach(b => { b.classList.toggle('active', b.dataset.topic === topic); }); + const content = document.querySelector('#docsView .docs-content'); + if (content) content.scrollTop = 0; + } + // Bootstrap + applyDocsRoute(); loadMeta().then(loadAvailableModels); loadSystemSpecs(); loadResults(); diff --git a/package.json b/package.json index c03d921..d579281 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "performance" ], "author": "", - "license": "ISC", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "axios": "^1.13.2", "better-sqlite3": "^12.4.6", diff --git a/src/benchmark.ts b/src/benchmark.ts index dadb5ae..285938a 100644 --- a/src/benchmark.ts +++ b/src/benchmark.ts @@ -288,6 +288,10 @@ interface BenchmarkResult { timestamp: string; success: boolean; error?: string; + /** The exact prompt sent to the model for this run. */ + prompt?: string; + /** The model's generated response text (kept for side-by-side comparison and PDF export). */ + response?: string; } interface OllamaModel { @@ -360,7 +364,9 @@ export async function benchmarkModel(modelName: string, customPrompt?: string): totalTokens: totalTokens, durationSeconds: parseFloat(durationSeconds.toFixed(2)), timestamp: new Date().toISOString(), - success: true + success: true, + prompt: promptToUse, + response: responseText }; } catch (error) { console.error(` ✗ Error benchmarking ${modelName}: ${(error as Error).message}`); @@ -371,7 +377,9 @@ export async function benchmarkModel(modelName: string, customPrompt?: string): durationSeconds: 0, timestamp: new Date().toISOString(), success: false, - error: (error as Error).message + error: (error as Error).message, + prompt: promptToUse, + response: '' }; } }