From 26ec4e3e6805c13e6aef5a43136fe35f3125e323 Mon Sep 17 00:00:00 2001 From: jenya Date: Wed, 30 Sep 2026 16:27:25 +0300 Subject: [PATCH] Playground: custom language dropdown; README and crate metadata for search - language picker is a styled button and listbox with a correctly placed chevron, keyboard support, outside click to close and RTL layout - other selects use a CSS chevron on the right side (left in RTL) - README: keyword-rich title, intro, headings and image alt text - Cargo.toml: description, homepage, keywords and categories --- Cargo.toml | 6 +++- README.md | 47 +++++++++++++++------------ demo/app.js | 84 +++++++++++++++++++++++++++++++++++++++++++++---- demo/index.html | 10 +++++- demo/style.css | 67 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 181 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 08d91bc..b24d865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,11 @@ name = "eigentrust" version = "0.1.0" edition = "2021" authors = ["Jenya "] -description = "Rust and WASM Eigentrust implementation" +description = "Fast EigenTrust reputation algorithm: global trust scores for peer-to-peer networks and social graphs, native and WebAssembly" +homepage = "https://eigentrust.jenyadoesapps.com" +readme = "README.md" +keywords = ["eigentrust", "reputation", "trust", "sybil", "wasm"] +categories = ["algorithms", "wasm", "science"] repository = "https://github.com/hypnagonia/eigentrust-rust" [features] diff --git a/README.md b/README.md index 453d706..1bf4e58 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -

EigenTrust

+

EigenTrust in Rust and WebAssembly

- Global trust scores from local trust. Fast, in Rust and WebAssembly. + A fast implementation of the EigenTrust reputation algorithm.
+ Global trust scores for peer-to-peer networks, social graphs and web-of-trust systems, with Sybil attack resistance built in.

@@ -12,24 +13,31 @@

- EigenTrust playground + EigenTrust playground: an interactive trust graph ranked live in the browser with WebAssembly

-## What it does +## What is EigenTrust? -- **Input:** who trusts whom (`alice,bob,3`), plus a few seed peers you already trust. -- **Output:** a score for every peer. The scores add up to 1. -- **Why:** trust spreads from the seeds, so fake accounts that only vouch for each other get almost nothing. +EigenTrust turns local trust ("alice trusts bob") into a global reputation score for every peer. Trust spreads from a few seed peers you already trust, so fake accounts that only vouch for each other get almost nothing. -Based on the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar et al., 2003). +- **Input:** who trusts whom (`alice,bob,3`), plus seed peers. +- **Output:** a trust score for every peer. The scores add up to 1. +- **Algorithm:** power iteration on a sparse trust matrix, as in the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar, Schlosser, Garcia-Molina, 2003). With no seeds it behaves like PageRank. -## Try it +## Use cases -**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: edit a network, move the α slider, see the ranking update. You can also rank 250,000 peers in the browser. +- Reputation systems for peer-to-peer and decentralized networks +- Sybil and spam resistance in social graphs and communities +- Ranking accounts, contributors or nodes by who vouches for them +- Web of trust and endorsement graphs -Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語. The language follows your browser; you can change it in the header or with `?lang=`. +## Live demo -## Command line +**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: build a trust network, move the α slider and watch the ranking update. You can also rank 250,000 peers in the browser. + +Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語. + +## Quick start (CLI) ```sh cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv [alpha] @@ -42,7 +50,7 @@ bob,0.3333333134651184 α defaults to `0.5`. A higher α keeps trust closer to the seeds. -## Browser +## Use in the browser (WebAssembly) ```js import init, { run } from './pkg/eigentrust.js' @@ -60,15 +68,14 @@ Build with `./build.sh`. For a ready-made Web Worker, see [`demo/worker.js`](dem | File | Line | Example | | --- | --- | --- | | Local trust | `from,to[,weight]` | `alice,bob,2` | -| Seeds | `peer[,weight]` | `alice,1` | +| Seeds (pre-trust) | `peer[,weight]` | `alice,1` | - A header row is optional. Spaces, quoted fields, CRLF and a UTF-8 BOM are fine. - Weights default to 1. Repeated `from,to` pairs: the last line wins. -- With no seeds, every peer starts equal (like PageRank). ## Performance -Random networks, 10 links per peer, CSV parsing included. +Random trust graphs, 10 links per peer, CSV parsing included. | Peers | Links | CLI | Browser | | ---: | ---: | ---: | ---: | @@ -76,13 +83,13 @@ Random networks, 10 links per peer, CSV parsing included. | 100,000 | 1,000,000 | 0.4 s | 0.2 s | | 250,000 | 2,500,000 | | 0.6 s | -The browser build uses all CPU cores when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)). +The browser build is multithreaded when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)). ## Development ```sh -cargo test --release # tests -./build.sh # WASM builds, copied into demo/ -python3 -m http.server -d demo # run the playground locally +cargo test --release # tests +./build.sh # WASM builds, copied into demo/ +python3 -m http.server -d demo # run the playground locally git config core.hooksPath .githooks # once per clone ``` diff --git a/demo/app.js b/demo/app.js index a34c814..27bcb8f 100644 --- a/demo/app.js +++ b/demo/app.js @@ -509,7 +509,7 @@ canvas.addEventListener('pointerup', (ev) => { }) document.addEventListener('keydown', (ev) => { - if (ev.target.closest('input, textarea, select')) return + if (ev.target.closest('input, textarea, select, [role="listbox"]')) return if (ev.key === 'Escape') select(null) if ((ev.key === 'Delete' || ev.key === 'Backspace') && state.selected) removePeer(state.selected) if (ev.key === 's' && state.selected) toggleSeed(state.selected) @@ -907,17 +907,89 @@ function localizeNumbers() { function applyLanguage(code) { setLanguage(code) - $('lang').value = lang() + renderLangMenu() localizeNumbers() setAlpha(state.alpha) renderPanel() wake() } -$('lang').replaceChildren(...Object.entries(LANGUAGES).map(([code, name]) => el('option', { value: code, lang: code }, name))) -$('lang').addEventListener('change', (ev) => { - saveLanguage(ev.target.value) - applyLanguage(ev.target.value) +// language menu: a button that opens a listbox, keyboard and pointer friendly +const langButton = $('langButton') +const langList = $('langList') +const langCodes = Object.keys(LANGUAGES) +let langActive = 0 + +function renderLangMenu() { + const cur = lang() + $('langCurrent').textContent = LANGUAGES[cur] + $('langCode').textContent = cur.toUpperCase() + langButton.setAttribute('aria-label', `${t('language')}: ${LANGUAGES[cur]}`) + langList.setAttribute('aria-label', t('language')) + langList.replaceChildren(...langCodes.map((code, i) => el('li', { + id: 'lang-opt-' + code, + role: 'option', + 'aria-selected': String(code === cur), + onclick: () => chooseLang(code), + onpointermove: () => setLangActive(i), + }, el('span', { lang: code, dir: 'auto' }, LANGUAGES[code]), code === cur ? checkIcon() : null))) +} + +function checkIcon() { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('viewBox', '0 0 12 12') + svg.setAttribute('width', '12') + svg.setAttribute('height', '12') + svg.setAttribute('aria-hidden', 'true') + svg.classList.add('check') + svg.innerHTML = '' + return svg +} + +function setLangActive(i) { + langActive = (i + langCodes.length) % langCodes.length + langList.querySelectorAll('li').forEach((li, j) => li.classList.toggle('is-active', j === langActive)) + langList.setAttribute('aria-activedescendant', 'lang-opt-' + langCodes[langActive]) +} + +function openLangMenu() { + langList.hidden = false + langButton.setAttribute('aria-expanded', 'true') + setLangActive(langCodes.indexOf(lang())) + langList.focus() +} + +function closeLangMenu(focusButton = true) { + if (langList.hidden) return + langList.hidden = true + langButton.setAttribute('aria-expanded', 'false') + if (focusButton) langButton.focus() +} + +function chooseLang(code) { + closeLangMenu() + if (code === lang()) return + saveLanguage(code) + applyLanguage(code) +} + +langButton.addEventListener('click', () => (langList.hidden ? openLangMenu() : closeLangMenu())) +langButton.addEventListener('keydown', (ev) => { + if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') { ev.preventDefault(); openLangMenu() } +}) +langList.addEventListener('keydown', (ev) => { + const keys = { ArrowDown: 1, ArrowUp: -1 } + if (ev.key in keys) setLangActive(langActive + keys[ev.key]) + else if (ev.key === 'Home') setLangActive(0) + else if (ev.key === 'End') setLangActive(langCodes.length - 1) + else if (ev.key === 'Enter' || ev.key === ' ') chooseLang(langCodes[langActive]) + else if (ev.key === 'Escape') closeLangMenu() + else if (ev.key === 'Tab') { closeLangMenu(false); return } + else return + ev.preventDefault() +}) +document.addEventListener('pointerdown', (ev) => { + if (!langList.hidden && !$('langMenu').contains(ev.target)) closeLangMenu(false) }) // ---------- start ---------- diff --git a/demo/index.html b/demo/index.html index 8ae0005..8c194a9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,7 +20,15 @@ EigenTrust