├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── assets ├── anagrams.png ├── bigrams.png ├── phrases.png └── words.png ├── components ├── ButtonAppBar.js ├── CC1ChordGenerator.js ├── ChordDiagnostic.css ├── ChordDiagnostic.js ├── ChordStatistics.js ├── ChordViewer.js ├── LinearWithValueLabel.js └── ToleranceRecommender.js ├── functions ├── anagramWorker.js ├── chordGenerationWorker.js ├── createCsv.js └── wordAnalysis.js ├── index.css ├── index.js ├── logo.svg ├── pages ├── CCXDebugging.js ├── ChordTools.js ├── HomePage.js ├── Practice.css ├── Practice.js └── WordTools.js ├── reportWebVitals.js ├── setupTests.js ├── theme.js └── words ├── english-1000.json ├── english-500.json └── english-5000.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CharaChorder Community Projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CharaChorder Utilities 2 | 3 | Welcome to the CharaChorder Utilities project! This project contains a set of tools for CharaChorder users. 4 | 5 | The best way to check them out is just by visiting the site here: [CharaChorder Utilities](https://typing-tech.github.io/CharaChorder-utilities/). 6 | 7 | ## Contributing 8 | 9 | Clone the repo and run: 10 | 11 | ### `npm install` 12 | 13 | Then, to start the server and begin developing run: 14 | 15 | ### `npm start` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 18 | 19 | The page will reload when you make changes.\ 20 | You may also see any lint errors in the console. 21 | 22 | I am open to any contributions--if you have an improvement you would like to make you can make a PR, and I will review it. 23 | 24 | ## Reporting issues 25 | 26 | If you encounter any problems or have any suggestions for improvement, please open an issue. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charachorder-utilities", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": "https://typing-tech.github.io/CharaChorder-utilities", 6 | "dependencies": { 7 | "@emotion/react": "^11.11.1", 8 | "@emotion/styled": "^11.11.0", 9 | "@mui/icons-material": "^5.14.6", 10 | "@mui/material": "^5.14.6", 11 | "@mui/x-data-grid": "^6.12.1", 12 | "@testing-library/jest-dom": "^6.1.2", 13 | "@testing-library/react": "^14.0.0", 14 | "@testing-library/user-event": "^14.4.3", 15 | "gh-pages": "^6.0.0", 16 | "papaparse": "^5.4.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-router-dom": "^6.15.0", 20 | "react-scripts": "^5.0.1", 21 | "vis-data": "^7.1.6", 22 | "vis-timeline": "^7.7.2", 23 | "web-vitals": "^3.4.0", 24 | "workerize-loader": "^2.0.2" 25 | }, 26 | "scripts": { 27 | "predeploy": "npm run build", 28 | "deploy": "gh-pages -d build", 29 | "start": "react-scripts start", 30 | "build": "react-scripts build" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | CharaChorder Utilities 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CharaChorder Utilities", 3 | "name": "CharaChorder Utilities", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HashRouter as Router, Route, Routes } from 'react-router-dom'; 3 | import { Container, Box } from '@mui/material'; 4 | import ButtonAppBar from './components/ButtonAppBar'; 5 | import ChordTools from './pages/ChordTools'; 6 | import WordTools from './pages/WordTools'; 7 | import Practice from './pages/Practice'; 8 | import HomePage from './pages/HomePage'; 9 | import CCX from './pages/CCXDebugging'; 10 | 11 | export default function App() { 12 | const [chordLibrary, setChordLibrary] = React.useState( 13 | JSON.parse(localStorage.getItem("chordLibrary")) || [] 14 | ); 15 | 16 | React.useEffect(() => { 17 | localStorage.setItem("chordLibrary", JSON.stringify(chordLibrary)); 18 | }, [chordLibrary]); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | 32 | 33 | 34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/assets/anagrams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/src/assets/anagrams.png -------------------------------------------------------------------------------- /src/assets/bigrams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/src/assets/bigrams.png -------------------------------------------------------------------------------- /src/assets/phrases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/src/assets/phrases.png -------------------------------------------------------------------------------- /src/assets/words.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typing-tech/CharaChorder-utilities/1fde3036f622d98d5b5e6101121a1d838bcf1bcf/src/assets/words.png -------------------------------------------------------------------------------- /src/components/ButtonAppBar.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AppBar, Box, Toolbar, Typography, Menu, MenuItem, Container, Button, Tooltip } from '@mui/material'; 3 | import { Dialog, DialogActions, DialogContent, DialogTitle, DialogContentText, Input } from '@mui/material'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import MenuIcon from '@mui/icons-material/Menu'; 6 | import KeyboardAltIcon from '@mui/icons-material/KeyboardAlt'; 7 | import SettingsIcon from '@mui/icons-material/Settings'; 8 | import { Link } from 'react-router-dom'; 9 | 10 | const pages = ['Word Tools', 'Chord Tools', 'Practice', 'CCX Debugging']; 11 | 12 | const actionCodes = { 13 | 32: ' ', 14 | 33: '!', 15 | 34: '"', 16 | 35: '#', 17 | 36: '$', 18 | 37: '%', 19 | 38: '&', 20 | 39: '\'', 21 | 40: '(', 22 | 41: ')', 23 | 42: '*', 24 | 43: '+', 25 | 44: ',', 26 | 45: '-', 27 | 46: '.', 28 | 47: '/', 29 | 48: '0', 30 | 49: '1', 31 | 50: '2', 32 | 51: '3', 33 | 52: '4', 34 | 53: '5', 35 | 54: '6', 36 | 55: '7', 37 | 56: '8', 38 | 57: '9', 39 | 58: ':', 40 | 59: ';', 41 | 60: '<', 42 | 61: '=', 43 | 62: '>', 44 | 63: '?', 45 | 64: '@', 46 | 65: 'A', 47 | 66: 'B', 48 | 67: 'C', 49 | 68: 'D', 50 | 69: 'E', 51 | 70: 'F', 52 | 71: 'G', 53 | 72: 'H', 54 | 73: 'I', 55 | 74: 'J', 56 | 75: 'K', 57 | 76: 'L', 58 | 77: 'M', 59 | 78: 'N', 60 | 79: 'O', 61 | 80: 'P', 62 | 81: 'Q', 63 | 82: 'R', 64 | 83: 'S', 65 | 84: 'T', 66 | 85: 'U', 67 | 86: 'V', 68 | 87: 'W', 69 | 88: 'X', 70 | 89: 'Y', 71 | 90: 'Z', 72 | 91: '[', 73 | 92: '\\', 74 | 93: ']', 75 | 94: '^', 76 | 95: '_', 77 | 96: '`', 78 | 97: 'a', 79 | 98: 'b', 80 | 99: 'c', 81 | 100: 'd', 82 | 101: 'e', 83 | 102: 'f', 84 | 103: 'g', 85 | 104: 'h', 86 | 105: 'i', 87 | 106: 'j', 88 | 107: 'k', 89 | 108: 'l', 90 | 109: 'm', 91 | 110: 'n', 92 | 111: 'o', 93 | 112: 'p', 94 | 113: 'q', 95 | 114: 'r', 96 | 115: 's', 97 | 116: 't', 98 | 117: 'u', 99 | 118: 'v', 100 | 119: 'w', 101 | 120: 'x', 102 | 121: 'y', 103 | 122: 'z', 104 | 123: '{', 105 | 124: '|', 106 | 125: '}', 107 | 126: '~', 108 | 128: '€', 109 | 130: '‚', 110 | 131: 'ƒ', 111 | 132: '„', 112 | 133: '…', 113 | 134: '†', 114 | 135: '‡', 115 | 136: 'ˆ', 116 | 137: '‰', 117 | 138: 'Š', 118 | 139: '‹', 119 | 140: 'Œ', 120 | 142: 'Ž', 121 | 145: '‘', 122 | 146: '’', 123 | 147: '“', 124 | 148: '”', 125 | 149: '•', 126 | 150: '–', 127 | 151: '—', 128 | 152: '˜', 129 | 153: '™', 130 | 154: 'š', 131 | 155: '›', 132 | 156: 'œ', 133 | 157: '', 134 | 158: 'ž', 135 | 159: 'Ÿ', 136 | 160: ' ', 137 | 161: '¡', 138 | 162: '¢', 139 | 163: '£', 140 | 164: '¤', 141 | 165: '¥', 142 | 166: '¦', 143 | 167: '§', 144 | 168: '¨', 145 | 169: '©', 146 | 170: 'ª', 147 | 171: '«', 148 | 172: '¬', 149 | 173: '­', 150 | 174: '®', 151 | 175: '¯', 152 | 176: '°', 153 | 177: '±', 154 | 178: '²', 155 | 179: '³', 156 | 180: '´', 157 | 181: 'µ', 158 | 182: '¶', 159 | 183: '·', 160 | 184: '¸', 161 | 185: '¹', 162 | 186: 'º', 163 | 187: '»', 164 | 188: '¼', 165 | 189: '½', 166 | 190: '¾', 167 | 191: '¿', 168 | 192: 'À', 169 | 193: 'Á', 170 | 194: 'Â', 171 | 195: 'Ã', 172 | 196: 'Ä', 173 | 197: 'Å', 174 | 198: 'Æ', 175 | 199: 'Ç', 176 | 200: 'È', 177 | 201: 'É', 178 | 202: 'Ê', 179 | 203: 'Ë', 180 | 204: 'Ì', 181 | 205: 'Í', 182 | 206: 'Î', 183 | 207: 'Ï', 184 | 208: 'Ð', 185 | 209: 'Ñ', 186 | 210: 'Ò', 187 | 211: 'Ó', 188 | 212: 'Ô', 189 | 213: 'Õ', 190 | 214: 'Ö', 191 | 215: '×', 192 | 216: 'Ø', 193 | 217: 'Ù', 194 | 218: 'Ú', 195 | 219: 'Û', 196 | 220: 'Ü', 197 | 221: 'Ý', 198 | 222: 'Þ', 199 | 223: 'ß', 200 | 224: 'à', 201 | 225: 'á', 202 | 226: 'â', 203 | 227: 'ã', 204 | 228: 'ä', 205 | 229: 'å', 206 | 230: 'æ', 207 | 231: 'ç', 208 | 232: 'è', 209 | 233: 'é', 210 | 234: 'ê', 211 | 235: 'ë', 212 | 236: 'ì', 213 | 237: 'í', 214 | 238: 'î', 215 | 239: 'ï', 216 | 240: 'ð', 217 | 241: 'ñ', 218 | 242: 'ò', 219 | 243: 'ó', 220 | 244: 'ô', 221 | 245: 'õ', 222 | 246: 'ö', 223 | 247: '÷', 224 | 248: 'ø', 225 | 249: 'ù', 226 | 250: 'ú', 227 | 251: 'û', 228 | 252: 'ü', 229 | 253: 'ý', 230 | 254: 'þ', 231 | 255: 'ÿ', 232 | 260: 'a', 233 | 261: 'b', 234 | 262: 'c', 235 | 263: 'd', 236 | 264: 'e', 237 | 265: 'f', 238 | 266: 'g', 239 | 267: 'h', 240 | 268: 'i', 241 | 269: 'j', 242 | 270: 'k', 243 | 271: 'l', 244 | 272: 'm', 245 | 273: 'n', 246 | 274: 'o', 247 | 275: 'p', 248 | 276: 'q', 249 | 277: 'r', 250 | 278: 's', 251 | 279: 't', 252 | 280: 'u', 253 | 281: 'v', 254 | 282: 'w', 255 | 283: 'x', 256 | 284: 'y', 257 | 285: 'z', 258 | 286: '1', 259 | 287: '2', 260 | 288: '3', 261 | 289: '4', 262 | 290: '5', 263 | 291: '6', 264 | 292: '7', 265 | 293: '8', 266 | 294: '9', 267 | 295: '0', 268 | 299: ' ', 269 | 300: ' ', 270 | 301: '-', 271 | 302: '=', 272 | 303: '[', 273 | 304: ']', 274 | 305: '\\', 275 | 306: '#', 276 | 307: ';', 277 | 308: '', 278 | 309: '`', 279 | 310: ',', 280 | 311: '.', 281 | 312: '/', 282 | 461: ' ', 283 | 536: 'DUP', 284 | 544: ' ' 285 | }; 286 | 287 | function ButtonAppBar({ chordLibrary, setChordLibrary }) { 288 | const [anchorElNav, setAnchorElNav] = React.useState(null); 289 | const [anchorElUser, setAnchorElUser] = React.useState(null); 290 | const [openModal, setOpenModal] = React.useState(false); 291 | const [selectedFile, setSelectedFile] = React.useState(null); 292 | const [chordInfoMessage, setChordInfoMessage] = React.useState(null); 293 | 294 | const handleOpenNavMenu = (event) => { 295 | setAnchorElNav(event.currentTarget); 296 | }; 297 | 298 | const handleOpenUserMenu = (event) => { 299 | setAnchorElUser(event.currentTarget); 300 | }; 301 | 302 | const handleCloseNavMenu = () => { 303 | setAnchorElNav(null); 304 | }; 305 | 306 | const handleCloseUserMenu = () => { 307 | setAnchorElUser(null); 308 | }; 309 | 310 | const handleOpenModal = () => { 311 | setOpenModal(true); 312 | }; 313 | 314 | const handleCloseModal = () => { 315 | setOpenModal(false); 316 | }; 317 | 318 | const parseChordsFromJSON = (jsonString, callback) => { 319 | const data = JSON.parse(jsonString); 320 | const chordsData = data.chords; 321 | 322 | const chords = chordsData.reduce((acc, chordPair) => { 323 | if (chordPair.length >= 2) { 324 | let [chordInput, chordOutput] = chordPair; 325 | if (typeof chordOutput !== 'undefined') { 326 | // Convert the action codes to characters 327 | chordInput = chordInput.filter(code => code !== 0).map(code => actionCodes[code]).filter(Boolean).join('+'); 328 | chordOutput = chordOutput.map(code => actionCodes[code]).filter(Boolean).join(''); 329 | acc.push({ chordInput, chordOutput }); 330 | } 331 | } 332 | return acc; 333 | }, []); 334 | 335 | callback(chords); 336 | }; 337 | 338 | 339 | const handleFileChange = (e) => { 340 | setSelectedFile(e.target.files[0]); 341 | if (e.target.files[0]) { 342 | const reader = new FileReader(); 343 | reader.onload = function (event) { 344 | const jsonString = event.target.result; 345 | parseChordsFromJSON(jsonString, (chords) => { 346 | if (chords.length === 0) { 347 | setChordInfoMessage("The chord file is either invalid or empty."); 348 | } else { 349 | setChordInfoMessage(`Parsed ${chords.length} chords from the file.`); 350 | } 351 | }); 352 | }; 353 | reader.readAsText(e.target.files[0]); 354 | } 355 | }; 356 | 357 | 358 | const handleFileUpload = () => { 359 | if (selectedFile) { 360 | const reader = new FileReader(); 361 | reader.onload = function (event) { 362 | const jsonString = event.target.result; 363 | parseChordsFromJSON(jsonString, (chords) => { 364 | if (chords.length > 0) { 365 | setChordLibrary(chords); 366 | } 367 | }); 368 | }; 369 | reader.readAsText(selectedFile); 370 | } 371 | setChordInfoMessage(null); 372 | handleCloseModal(); 373 | }; 374 | 375 | return ( 376 |
377 | 378 | 379 | 380 | 381 | 395 | CharaChorder Utilities 396 | 397 | 398 | 399 | 400 | 408 | 409 | 410 | 411 | 429 | {pages.map((page) => ( 430 | 433 | ))} 434 | 435 | 436 | 437 | 453 | CC Utilities 454 | 455 | 456 | {pages.map((page) => ( 457 | 460 | ))} 461 | 462 | 470 | Chords: {chordLibrary.length} 471 | 472 | 473 | 474 | 475 | 476 | 477 | 493 | { 494 | { handleOpenModal(); handleCloseUserMenu(); }} 497 | > 498 | Load Chord Library 499 | 500 | } 501 | 502 | 503 | 504 | 505 | 506 | 507 | Upload Chord Library File 508 | 509 | Browse for your exported Chord Backup from Device Manager for use in the Chord Tools and Practice. 510 | 511 | 512 | {chordInfoMessage} 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 |
521 | ); 522 | } 523 | export default ButtonAppBar; -------------------------------------------------------------------------------- /src/components/CC1ChordGenerator.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { 3 | Table, TableBody, TableCell, TableHead, TableRow, TextField, Button, 4 | Select, MenuItem, Slider, Typography, FormControlLabel, Switch, Divider, Box, 5 | InputAdornment 6 | } from '@mui/material'; 7 | import IconButton from '@mui/material/IconButton'; 8 | import ClearIcon from '@mui/icons-material/Clear'; 9 | import LinearWithValueLabel from '../components/LinearWithValueLabel'; 10 | import Papa from 'papaparse'; 11 | import english500 from '../words/english-500.json'; 12 | import english1000 from '../words/english-1000.json'; 13 | import english5000 from '../words/english-5000.json'; 14 | import ChordWorker from 'workerize-loader!../functions/chordGenerationWorker'; // eslint-disable-line import/no-webpack-loader-syntax 15 | import createCsv from '../functions/createCsv'; 16 | 17 | function CC1ChordGenerator({ chordLibrary }) { 18 | const [generatingChords, setGeneratingChords] = useState(false); 19 | const [generationProgress, setGenerationProgress] = useState(0); 20 | const [createdChords, setCreatedChords] = useState({}); 21 | const [failedWords, setFailedWords] = useState([]); 22 | const [skippedWordCount, setSkippedWordCount] = useState([]); 23 | 24 | const [inputWords, setInputWords] = useState(''); 25 | const [sliderValue, setSliderValue] = useState([3, 6]); 26 | const [checkboxStates, setCheckboxStates] = useState({ 27 | useDupKey: true, 28 | useMirroredKeys: true, 29 | useAltKeys: false, 30 | use3dKeys: false, 31 | }); 32 | const chordGeneratorWorker = new ChordWorker(); 33 | 34 | // CSV state 35 | const [csvWords, setCsvWords] = useState([]); 36 | const [numRowsToUse, setNumRowsToUse] = useState(0); 37 | const [isFileUploaded, setIsFileUploaded] = useState(false); 38 | const handleCsvUpload = (e) => { 39 | const file = e.target.files[0]; 40 | Papa.parse(file, { 41 | complete: function (results) { 42 | const wordsFromCsv = results.data.map(row => row[0]).filter(Boolean); 43 | setCsvWords(wordsFromCsv); 44 | setIsFileUploaded(true); 45 | } 46 | }); 47 | }; 48 | const fileInputRef = useRef(null); 49 | 50 | // Word set dropdown state 51 | const [selectedWordSet, setSelectedWordSet] = useState(''); 52 | 53 | const clearCsv = () => { 54 | setNumRowsToUse(0); 55 | setCsvWords([]); 56 | setIsFileUploaded(false); 57 | if (fileInputRef.current) { 58 | fileInputRef.current.value = null; 59 | } 60 | }; 61 | 62 | const handleChange = (e) => { 63 | setInputWords(e.target.value); 64 | }; 65 | 66 | const clearInput = () => { 67 | setInputWords(''); 68 | }; 69 | 70 | const handleSliderChange = (event, newValue) => { 71 | setSliderValue(newValue); 72 | }; 73 | 74 | const handleCheckboxChange = (event) => { 75 | setCheckboxStates({ 76 | ...checkboxStates, 77 | [event.target.name]: event.target.checked, 78 | }); 79 | }; 80 | 81 | const handleClick = async () => { 82 | const wordsArray = csvWords.length > 0 ? csvWords.slice(0, numRowsToUse) : inputWords.split(',').map(word => word.trim()); 83 | setGeneratingChords(true); 84 | chordGeneratorWorker.postMessage({ 85 | type: 'generateChords', 86 | wordsArray: wordsArray, 87 | sliderValue: sliderValue, 88 | checkboxStates: checkboxStates, 89 | chordLibrary: chordLibrary 90 | }); 91 | }; 92 | 93 | chordGeneratorWorker.addEventListener('message', (event) => { 94 | if (event.data.type === 'progress') { 95 | setGenerationProgress(event.data.progress); 96 | } 97 | if (event.data.type === 'result') { 98 | let cChords = event.data.result.usedChords; 99 | let skippedChords = event.data.result.skippedWordCount 100 | let fWords = event.data.result.failedWords; 101 | setFailedWords(fWords); 102 | setSkippedWordCount(skippedChords); 103 | setCreatedChords(cChords); 104 | setGeneratingChords(false); 105 | } 106 | }); 107 | 108 | const wordSets = { 109 | 'english500': english500.words, 110 | 'english1000': english1000.words, 111 | 'english5000': english5000.words, 112 | }; 113 | 114 | useEffect(() => { 115 | setNumRowsToUse(Math.min(200, csvWords.length)); 116 | if (wordSets[selectedWordSet]) { 117 | setInputWords(wordSets[selectedWordSet].join(', ')); 118 | } 119 | 120 | // eslint-disable-next-line react-hooks/exhaustive-deps 121 | }, [csvWords, selectedWordSet]); 122 | 123 | const downloadCsv = (chords) => { 124 | const dataArray = Object.entries(chords).map(([word, chord]) => ({ 125 | chord: chord.join(' + '), 126 | word 127 | })); 128 | 129 | createCsv(dataArray, 'chords.csv', false); 130 | } 131 | 132 | const createdChordsArray = Object.entries(createdChords); 133 | 134 | return ( 135 |
136 | 137 | Either upload a .csv file that has words sorted by importance in the first column (such as a CharaChorder Nexus export), 138 | type words separated by commas, or choose from a predetermined set of word lists. Note: using the word lists can take 139 | quite a bit of time, so you may have to wait a few minutes. 140 | 141 | 142 | 146 | {csvWords.length > 0 && ( 147 | <> 148 | {csvWords.length} words loaded. How many do you want to generate chords for? 149 | setNumRowsToUse(Number(e.target.value))} 154 | InputProps={{ 155 | inputProps: { 156 | min: 0, 157 | step: 50, 158 | max: csvWords.length 159 | } 160 | }} 161 | /> 162 | 165 | 166 | )} 167 | 168 | 169 | {csvWords.length === 0 && ( 170 | <> 171 | 172 | - or - 173 | 174 | 184 | 185 | 186 | 187 | 188 | ) 189 | }} 190 | /> 191 | 203 | 204 | )} 205 | 206 | 207 | 208 | 209 | Select number of keys for the chord inputs 210 | 218 | Selected range: {sliderValue[0]} - {sliderValue[1]} 219 |
220 | {["useDupKey", "useMirroredKeys", "useAltKeys", "use3dKeys"].map((key) => ( 221 | 228 | } 229 | label={key.replace(/([a-z0-9])([A-Z])/g, '$1 $2')} 230 | key={key} 231 | /> 232 | ))} 233 |
234 | 237 | { 238 | generatingChords && ( 239 | <> 240 | Generating chords 241 | 242 | 243 | ) 244 | } 245 | 246 | { 247 | (createdChordsArray.length > 0) && ( 248 | <> 249 | 252 | Results 253 | { 254 | (skippedWordCount > 0) && ( 255 | <> 256 | {skippedWordCount} words were already in your chord library and were skipped. 257 | 258 | ) 259 | } 260 | { 261 | (failedWords.length > 0) && ( 262 | <> 263 | 264 | {failedWords.length} word{failedWords.length > 1 ? 's' : ''} did not have {failedWords.length === 1 ? 'a' : ''} valid chord{failedWords.length > 1 ? 's ' : ''} generated: {failedWords.join(', ')} 265 | 266 | 267 | ) 268 | } 269 | {createdChordsArray.length} chords created 270 | 271 | 272 | 273 | Word 274 | Chord 275 | 276 | 277 | 278 | {createdChordsArray.map(([word, chord], index) => { 279 | return ( 280 | 281 | {word} 282 | {chord.join(' + ')} 283 | 284 | ); 285 | })} 286 | 287 |
288 | 289 | 290 | ) 291 | } 292 | 293 |
294 | ); 295 | } 296 | 297 | export default CC1ChordGenerator; -------------------------------------------------------------------------------- /src/components/ChordDiagnostic.css: -------------------------------------------------------------------------------- 1 | #wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | align-items: center; 6 | } 7 | 8 | #wrapper #visualization { 9 | flex: 1 1; 10 | min-height: 0px; 11 | min-width: 100%; 12 | } 13 | 14 | #visualization { 15 | height: 100%; 16 | text-align: left; 17 | } 18 | 19 | .vis-item.green { 20 | background-color: green; 21 | border-color: green; 22 | } 23 | 24 | .vis-item.red { 25 | background-color: red; 26 | border-color: red; 27 | } 28 | 29 | #timing-table { 30 | width: 100%; 31 | border-collapse: collapse; 32 | } 33 | 34 | #timing-table th { 35 | padding: 8px; 36 | text-align: left; 37 | font-weight: bold; 38 | } -------------------------------------------------------------------------------- /src/components/ChordDiagnostic.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { Button, Container, TextField, Table, TableBody, TableCell, TableHead, TableRow, Typography, Box } from '@mui/material'; 3 | import 'vis-timeline/dist/vis-timeline-graph2d.min.css'; 4 | import './ChordDiagnostic.css'; 5 | import { DataSet } from "vis-data/peer"; 6 | import { Timeline } from "vis-timeline"; 7 | 8 | const TableRowComponent = ({ event, time, startTime }) => { 9 | const relativeTime = time - startTime; 10 | const eventType = event.type === 'keydown' ? 'Press' : 'Release'; 11 | return ( 12 | 13 | {eventType} 14 | {event.code} 15 | {relativeTime.toFixed(2) + ' ms'} 16 | 17 | ); 18 | }; 19 | 20 | const ChordDiagnostic = () => { 21 | const containerRef = useRef(null); 22 | const [events, setEvents] = useState([]); 23 | const [pressMessage, setPressMessage] = useState(''); 24 | const [releaseMessage, setReleaseMessage] = useState(''); 25 | const [pressToReleaseMessage, setPressToReleaseMessage] = useState(''); 26 | const timelineRef = useRef(null); 27 | const [textFieldValue, setTextFieldValue] = useState(''); 28 | const plotTimeoutRef = useRef(null); 29 | 30 | const handleKeyEvents = (event) => { 31 | setEvents([...events, { event, time: performance.now() }]); 32 | setTextFieldValue(event.target.value); 33 | 34 | if (plotTimeoutRef.current) { 35 | clearTimeout(plotTimeoutRef.current); 36 | } 37 | 38 | const delay = 500; 39 | plotTimeoutRef.current = setTimeout(handlePlot, delay); 40 | }; 41 | 42 | // Clear the timeout when the component is unmounted 43 | useEffect(() => { 44 | return () => { 45 | if (plotTimeoutRef.current) { 46 | clearTimeout(plotTimeoutRef.current); 47 | } 48 | }; 49 | }, []); 50 | 51 | const handlePlot = () => { 52 | const container = containerRef.current; 53 | const initialTime = events[0].time; 54 | 55 | const items = new DataSet(events.map(({ event, time }) => { 56 | const color = event.type === 'keyup' ? 'red' : 'green'; 57 | const relativeTime = time - initialTime; 58 | return { 59 | content: event.code, 60 | start: relativeTime, 61 | title: `${relativeTime.toFixed(2)} ms`, 62 | style: `color: white`, 63 | className: color 64 | }; 65 | })); 66 | 67 | const options = { 68 | orientation: 'top', 69 | align: 'left', 70 | order: (a, b) => b.time - a.time, 71 | showMajorLabels: false, 72 | format: { 73 | minorLabels: (date) => date.valueOf().toString(), 74 | } 75 | }; 76 | 77 | // Clearing the previous timeline 78 | container.innerHTML = ''; 79 | 80 | // Creating the new timeline 81 | const timeline = new Timeline(container, items, options); 82 | timelineRef.current = timeline; 83 | 84 | // Filter the events to only include those before the first Backspace 85 | const filteredEvents = []; 86 | for (const eventObj of events) { 87 | if (eventObj.event.code === 'Backspace') break; // Stop processing after the first Backspace 88 | filteredEvents.push(eventObj); 89 | } 90 | 91 | let firstPressTime = null; 92 | let lastPressTime = null; 93 | let firstReleaseTime = null; 94 | let lastReleaseTime = null; 95 | let pressToReleaseTime = null; 96 | 97 | filteredEvents.forEach(({ event, time }) => { 98 | if (event.type === 'keydown') { 99 | if (firstPressTime === null) { 100 | firstPressTime = time; 101 | } 102 | lastPressTime = time; 103 | } else if (event.type === 'keyup') { 104 | if (firstReleaseTime === null) { 105 | firstReleaseTime = time; 106 | } 107 | lastReleaseTime = time; 108 | } 109 | }); 110 | 111 | const pressDifference = lastPressTime !== null && firstPressTime !== null 112 | ? (lastPressTime - firstPressTime).toFixed(2) + ' ms' 113 | : 'N/A'; 114 | 115 | const releaseDifference = lastReleaseTime !== null && firstReleaseTime !== null 116 | ? (lastReleaseTime - firstReleaseTime).toFixed(2) + ' ms' 117 | : 'N/A'; 118 | 119 | if (lastPressTime !== null && firstReleaseTime !== null) { 120 | pressToReleaseTime = (firstReleaseTime - lastPressTime).toFixed(2) + ' ms'; 121 | } else { 122 | pressToReleaseTime = 'N/A'; 123 | } 124 | 125 | // Setting messages 126 | setPressMessage('The time between the first and last press was ' + pressDifference + '.'); 127 | setReleaseMessage('The time between the first and last release was ' + releaseDifference + '.'); 128 | setPressToReleaseMessage('The time between the last press and the first release was ' + pressToReleaseTime + '.'); 129 | }; 130 | 131 | const handleReset = () => { 132 | if (timelineRef.current) { 133 | timelineRef.current.destroy(); // Destroy the timeline instance 134 | } 135 | setEvents([]); 136 | setPressMessage(''); 137 | setReleaseMessage(''); 138 | setTextFieldValue(''); 139 | setPressToReleaseMessage(''); 140 | }; 141 | 142 | return ( 143 | 144 |
145 |
146 | 147 | This tool is designed to help you look at the press and release timings of a chord. 148 | To use, try to chord the word in the input box and then shortly after it will be plotted. 149 | There is also a table of the presses and releases. 150 | Credit to Tangent Chang (andy23512) from the CharaChorder Discord for this tool. 151 | 152 |
153 |
154 | 155 | setTextFieldValue(e.target.value)} 159 | onKeyUp={handleKeyEvents} 160 | onKeyDown={handleKeyEvents} 161 | /> 162 | 163 | 164 |
165 |
166 |
167 |
168 | {pressMessage} 169 | {releaseMessage} 170 | {pressToReleaseMessage} 171 |
172 |
173 | 174 | 175 | 176 | Event 177 | Code 178 | Time 179 | 180 | 181 | 182 | {events.map((eventData, index) => ( 183 | 189 | ))} 190 | 191 |
192 |
193 |
194 |
195 | ); 196 | }; 197 | 198 | export default ChordDiagnostic; 199 | -------------------------------------------------------------------------------- /src/components/ChordStatistics.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; 3 | import { Snackbar, Alert } from '@mui/material'; 4 | 5 | const ChordStatistics = ({ chordLibrary }) => { 6 | const [chordStats, setChordStats] = useState({}); 7 | const [letterCounts, setLetterCounts] = useState([]); 8 | const [dupWords, setDupWords] = useState([]); 9 | const [uniqueChordOutputs, setUniqueChordOutputs] = useState(new Set()); 10 | const canvasRef = React.useRef(null); 11 | const [openSnackbar, setOpenSnackbar] = useState(false); 12 | 13 | const handleOpenSnackbar = () => { 14 | setOpenSnackbar(true); 15 | }; 16 | 17 | const handleCloseSnackbar = () => { 18 | setOpenSnackbar(false); 19 | }; 20 | 21 | useEffect(() => { 22 | const calculateStatistics = () => { 23 | const lengthCounts = {}; 24 | const chordMapCounts = {}; 25 | const lettersCounts = {}; 26 | const newDupWords = []; 27 | const uniqueChordOutputs = new Set(); 28 | 29 | // Adapted to work with array of objects 30 | chordLibrary.forEach(({ chordInput, chordOutput }) => { 31 | let chordMap = chordInput.replace(/[\s+]+/g, ' '); 32 | const chordMapLength = chordMap.split(' ').length; 33 | 34 | lengthCounts[chordMapLength] = (lengthCounts[chordMapLength] || 0) + 1; 35 | uniqueChordOutputs.add(chordOutput); 36 | const chord = chordOutput; 37 | 38 | chordMapCounts[chord] = (chordMapCounts[chord] || 0) + 1; 39 | 40 | if (chordMapCounts[chord] > 1) newDupWords.push(chord); 41 | 42 | const letters = chordMap.split(' '); 43 | letters.forEach(letter => { 44 | lettersCounts[letter] = (lettersCounts[letter] || 0) + 1; 45 | }); 46 | }); 47 | 48 | setUniqueChordOutputs(uniqueChordOutputs); 49 | setChordStats(lengthCounts); 50 | setLetterCounts(Object.entries(lettersCounts).sort((a, b) => b[1] - a[1])); 51 | setDupWords(newDupWords); 52 | }; 53 | 54 | calculateStatistics(); 55 | }, [chordLibrary]); 56 | 57 | useEffect(() => { 58 | const currentCanvasRef = canvasRef.current; 59 | if (!currentCanvasRef) return; 60 | 61 | const generateBannerContent = (numChords, numUniqueChords, lengthCounts) => { 62 | let xAxis = []; 63 | let counts = []; 64 | for (var length in lengthCounts) { 65 | xAxis.push(length); 66 | counts.push(lengthCounts[length]); 67 | } 68 | 69 | // Set the canvas size 70 | var canvas = document.createElement("canvas"); 71 | canvas.width = 250; 72 | canvas.height = 125; 73 | 74 | // Get the canvas context 75 | var ctx = canvas.getContext("2d"); 76 | 77 | // Set the font and text baseline 78 | ctx.font = "16px Georgia"; 79 | ctx.textBaseline = "top"; 80 | 81 | // To change the color on the rectangle, just manipulate the context 82 | ctx.strokeStyle = "rgb(255, 255, 255)"; 83 | ctx.fillStyle = "rgb(0, 0, 0)"; 84 | ctx.beginPath(); 85 | ctx.roundRect(3, 3, canvas.width - 5, canvas.height - 5, 10); 86 | ctx.stroke(); 87 | ctx.fill(); 88 | 89 | ctx.beginPath(); 90 | // Set the fill color to white 91 | ctx.fillStyle = "#FFFFFF"; 92 | 93 | // Draw the text on the canvas 94 | ctx.fillText("Number of chords: " + numChords, 10, 10); 95 | ctx.fillText("Number of unique words: " + numUniqueChords, 10, 30); 96 | 97 | // Set the font for the label text 98 | ctx.font = "12px Georgia"; 99 | 100 | // Measure the label text 101 | var labelText = "Generated with CharaChorder-utilities"; 102 | var labelWidth = ctx.measureText(labelText).width; 103 | 104 | ctx.fillStyle = "#666"; 105 | 106 | // Draw the label text at the bottom right corner of the canvas 107 | ctx.fillText(labelText, canvas.width - labelWidth - 10, canvas.height - 20); 108 | 109 | // Set the chart area width and height 110 | const chartWidth = 125; 111 | const chartHeight = 25; 112 | const labelHeight = 10; // height of the label area below the chart 113 | const columnSpacing = 2; // space between columns 114 | 115 | // Calculate the maximum count value 116 | const maxCount = Math.max(...counts); 117 | 118 | // Calculate the column width based on the number of columns and column spacing 119 | const columnWidth = (chartWidth - (counts.length - 1) * columnSpacing) / counts.length; 120 | 121 | // Set the starting x and y positions for the columns 122 | let xPos = 100; 123 | let yPos = canvas.height - 50; 124 | 125 | ctx.font = "12px monospace"; 126 | ctx.fillStyle = "white"; 127 | ctx.textAlign = "left"; 128 | ctx.textBaseline = "top"; 129 | ctx.fillText("Chord length", 5, yPos + labelHeight / 2); 130 | ctx.textAlign = "center"; 131 | // Iterate through the counts and draw the columns 132 | for (let i = 0; i < counts.length; i++) { 133 | // Calculate the column height based on the count value and the maximum count 134 | const columnHeight = (counts[i] / maxCount) * chartHeight; 135 | 136 | // Draw the column 137 | ctx.fillRect(xPos, yPos - columnHeight, columnWidth, columnHeight); 138 | 139 | // Draw the label below the column 140 | ctx.fillText(xAxis[i], xPos + columnWidth / 2, yPos + labelHeight / 2); 141 | 142 | // Increment the x position for the next column 143 | xPos += columnWidth; 144 | } 145 | 146 | return canvas; 147 | } 148 | 149 | // Function to handle clipboard operations 150 | const copyToClipboard = () => { 151 | const canvas = generateBannerContent(chordLibrary.length, uniqueChordOutputs.size, chordStats); 152 | canvas.toBlob((blob) => { 153 | const item = new ClipboardItem({ "image/png": blob }); 154 | navigator.clipboard.write([item]).then(() => { 155 | handleOpenSnackbar(); // Open Snackbar on successful clipboard write 156 | }); 157 | }); 158 | }; 159 | 160 | // Function to draw on canvas 161 | const drawOnCanvas = () => { 162 | const canvas = generateBannerContent(chordLibrary.length, uniqueChordOutputs.size, chordStats); 163 | const ctx = currentCanvasRef.getContext('2d'); 164 | ctx.clearRect(0, 0, currentCanvasRef.width, currentCanvasRef.height); 165 | ctx.drawImage(canvas, 0, 0); 166 | }; 167 | 168 | // Attach click event for clipboard operations 169 | currentCanvasRef.addEventListener("click", copyToClipboard); 170 | 171 | // Draw on canvas 172 | drawOnCanvas(); 173 | 174 | // Cleanup 175 | return () => { 176 | currentCanvasRef.removeEventListener("click", copyToClipboard); 177 | }; 178 | }, [chordLibrary, uniqueChordOutputs, chordStats]); 179 | 180 | return ( 181 |
182 | Click image to copy to clipboard 183 | 184 | Duplicate Words ({dupWords.length}): {dupWords.join(', ')} 185 |
186 | 187 | 188 | 189 | 190 | Chord Lengths 191 | Count 192 | 193 | 194 | 195 | {Object.keys(chordStats).map((length) => ( 196 | 197 | {length} key chord 198 | {chordStats[length]} 199 | 200 | ))} 201 | 202 |
203 |
204 |
205 | 206 | 207 | 208 | 209 | Letter/Key 210 | Count 211 | 212 | 213 | 214 | {letterCounts.map(([letter, count], index) => ( 215 | 216 | {letter} 217 | {count} 218 | 219 | ))} 220 | 221 |
222 |
223 | 229 | 230 | Copied to clipboard! 231 | 232 | 233 |
234 | ); 235 | } 236 | 237 | export default ChordStatistics; 238 | -------------------------------------------------------------------------------- /src/components/ChordViewer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Typography, FormControlLabel, Checkbox, Box } from '@mui/material'; 3 | import { DataGrid, GridToolbar } from '@mui/x-data-grid'; 4 | 5 | function ChordViewer({ chordLibrary = [] }) { 6 | const [filteredChords, setFilteredChords] = useState(chordLibrary); 7 | const [showDuplicates, setShowDuplicates] = useState(false); 8 | 9 | 10 | useEffect(() => { 11 | let filtered = chordLibrary; 12 | 13 | if (showDuplicates) { 14 | const outputCount = new Map(); 15 | filtered.forEach(chord => { 16 | const output = chord.chordOutput ? chord.chordOutput.toLowerCase() : ''; 17 | outputCount.set(output, (outputCount.get(output) || 0) + 1); 18 | }); 19 | 20 | filtered = filtered.filter(chord => { 21 | const output = chord.chordOutput ? chord.chordOutput.toLowerCase() : ''; 22 | return outputCount.get(output) > 1; 23 | }); 24 | 25 | filtered.sort((a, b) => { 26 | if (a.chordOutput < b.chordOutput) { 27 | return -1; 28 | } 29 | if (a.chordOutput > b.chordOutput) { 30 | return 1; 31 | } 32 | return 0; 33 | }); 34 | } 35 | 36 | setFilteredChords(filtered); 37 | }, [chordLibrary, showDuplicates]); 38 | 39 | const columns = [ 40 | { field: 'id', headerName: 'Chord Index', width: 100}, 41 | { field: 'chordInput', headerName: 'Chord Input', flex: 0.5 }, 42 | { field: 'chordOutput', headerName: 'Chord Output', flex: 1} 43 | ]; 44 | 45 | const rows = filteredChords.map((chord, index) => ({ 46 | id: index+1, 47 | chordInput: chord.chordInput, 48 | chordOutput: chord.chordOutput 49 | })); 50 | 51 | return ( 52 | <> 53 | {chordLibrary.length > 0 ? ( 54 | <> 55 | 56 | setShowDuplicates(!showDuplicates)} 61 | /> 62 | } 63 | label="Show only duplicates" 64 | /> 65 | 66 |
67 | 78 |
79 | 80 | ) : ( 81 | 82 | Please load your chord library in settings. 83 | 84 | )} 85 | 86 | ); 87 | }; 88 | 89 | export default ChordViewer; -------------------------------------------------------------------------------- /src/components/LinearWithValueLabel.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LinearProgress from '@mui/material/LinearProgress'; 4 | import Typography from '@mui/material/Typography'; 5 | import Box from '@mui/material/Box'; 6 | 7 | function LinearProgressWithLabel(props) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | {`${Math.round(props.value)}%`} 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | LinearProgressWithLabel.propTypes = { 23 | value: PropTypes.number.isRequired, // Value between 0 and 100. 24 | }; 25 | 26 | export default LinearProgressWithLabel; 27 | -------------------------------------------------------------------------------- /src/components/ToleranceRecommender.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Button, 4 | Container, 5 | Typography, 6 | Box, 7 | TextField, 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableRow, 13 | Paper 14 | } from '@mui/material'; 15 | 16 | /** 17 | * Winsorize an array by clamping values to the lowerPercentile and upperPercentile. 18 | * For example, (0.05, 0.95) would clamp the bottom 5% and top 5%. 19 | */ 20 | function winsorize(array, lowerPercent = 0.05, upperPercent = 0.95) { 21 | if (!array.length) return []; 22 | const sorted = [...array].sort((a, b) => a - b); 23 | const n = sorted.length; 24 | const lowerIndex = Math.floor(n * lowerPercent); 25 | const upperIndex = Math.floor(n * upperPercent); 26 | const lowerVal = sorted[lowerIndex]; 27 | const upperVal = sorted[Math.min(upperIndex, n - 1)]; 28 | 29 | return sorted.map((v) => Math.max(lowerVal, Math.min(v, upperVal))); 30 | } 31 | 32 | 33 | /** 34 | * Compute the mean of a winsorized array. 35 | */ 36 | function winsorizedMean(array, lowerPercent = 0.05, upperPercent = 0.95) { 37 | if (!array.length) return 0; 38 | const w = winsorize(array, lowerPercent, upperPercent); 39 | if (!w.length) return 0; 40 | const sum = w.reduce((acc, val) => acc + val, 0); 41 | return sum / w.length; 42 | } 43 | 44 | /** 45 | * Analyzes a single attempt: 46 | * - pressDifference (ms) = time between first & last keyDown 47 | * - releaseDifference (ms) = time between first & last keyUp 48 | * - scaled by (difference / numberOfKeys) 49 | */ 50 | function analyzeAttempt(events) { 51 | if (!events || !events.length) { 52 | return { 53 | pressDifference: 0, 54 | releaseDifference: 0, 55 | pressDifferenceScaled: 0, 56 | releaseDifferenceScaled: 0, 57 | keysPressed: 0, 58 | }; 59 | } 60 | 61 | const sorted = [...events].sort((a, b) => a.time - b.time); 62 | 63 | let firstPressTime = null; 64 | let lastPressTime = null; 65 | let firstReleaseTime = null; 66 | let lastReleaseTime = null; 67 | const pressedKeyCodes = new Set(); 68 | 69 | sorted.forEach(({ event, time }) => { 70 | if (event.type === 'keydown') { 71 | if (firstPressTime === null) { 72 | firstPressTime = time; 73 | } 74 | lastPressTime = time; 75 | pressedKeyCodes.add(event.code); 76 | } else if (event.type === 'keyup') { 77 | if (firstReleaseTime === null) { 78 | firstReleaseTime = time; 79 | } 80 | lastReleaseTime = time; 81 | } 82 | }); 83 | 84 | const pressDifference = 85 | (lastPressTime ?? 0) - (firstPressTime ?? 0); 86 | const releaseDifference = 87 | (lastReleaseTime ?? 0) - (firstReleaseTime ?? 0); 88 | 89 | const keysCount = pressedKeyCodes.size || 1; // avoid /0 90 | const pressDifferenceScaled = pressDifference / keysCount; 91 | const releaseDifferenceScaled = releaseDifference / keysCount; 92 | 93 | return { 94 | pressDifference, 95 | releaseDifference, 96 | pressDifferenceScaled, 97 | releaseDifferenceScaled, 98 | keysPressed: pressedKeyCodes.size, 99 | }; 100 | } 101 | 102 | const ToleranceRecommender = () => { 103 | // Completed attempts (array of arrays) 104 | const [attempts, setAttempts] = useState([]); 105 | // Current attempt in progress 106 | const [currentAttempt, setCurrentAttempt] = useState([]); 107 | // Track keys currently down 108 | const [pressedKeys, setPressedKeys] = useState(new Set()); 109 | 110 | const [attemptSummaries, setAttemptSummaries] = useState([]); 111 | // Our final recommended tolerance (median-based + WPM clamp) 112 | const [recommendations, setRecommendations] = useState({ 113 | pressMedian: 0, 114 | releaseMedian: 0, 115 | recommendedPress: 0, 116 | recommendedRelease: 0, 117 | }); 118 | 119 | const [textFieldValue, setTextFieldValue] = useState(''); 120 | // Field where user inputs typical WPM for normal typing 121 | const [wpm, setWpm] = useState('80'); 122 | 123 | /** 124 | * handleKeyEvents - capture keydown/keyUp 125 | */ 126 | const handleKeyEvents = (event) => { 127 | const now = performance.now(); 128 | setCurrentAttempt((prev) => [...prev, { event, time: now }]); 129 | 130 | if (event.type === 'keydown') { 131 | setPressedKeys((prev) => { 132 | const copy = new Set(prev); 133 | copy.add(event.code); 134 | return copy; 135 | }); 136 | } else if (event.type === 'keyup') { 137 | setPressedKeys((prev) => { 138 | const copy = new Set(prev); 139 | copy.delete(event.code); 140 | return copy; 141 | }); 142 | } 143 | }; 144 | 145 | const handleTextChange = (e) => { 146 | setTextFieldValue(e.target.value); 147 | }; 148 | 149 | /** 150 | * When pressedKeys becomes empty, finalize the attempt. 151 | * Also clear typed text so we don't accumulate "wtwtwtwt..." 152 | */ 153 | useEffect(() => { 154 | if (pressedKeys.size === 0 && currentAttempt.length > 0) { 155 | // Finalize attempt 156 | setAttempts((prev) => [...prev, currentAttempt]); 157 | setCurrentAttempt([]); 158 | setTextFieldValue(''); 159 | 160 | // Now that we added a new attempt, we can either 161 | // compute recommendations here immediately or 162 | // wait for attemptSummaries to re-render. 163 | setTimeout(() => { 164 | computeRecommendations(); 165 | }, 0); 166 | } // eslint-disable-next-line 167 | }, [pressedKeys, currentAttempt]); 168 | 169 | /** 170 | * Recompute attemptSummaries whenever attempts changes 171 | */ 172 | useEffect(() => { 173 | if (!attempts.length) { 174 | setAttemptSummaries([]); 175 | return; 176 | } 177 | const summaries = attempts.map((att) => analyzeAttempt(att)); 178 | setAttemptSummaries(summaries); 179 | }, [attempts]); 180 | 181 | /** 182 | * Compute median-based (scaled) recommendations, 183 | * clamp them by user’s typing speed if provided. 184 | */ 185 | const computeRecommendations = () => { 186 | if (!attemptSummaries.length) { 187 | return; 188 | } 189 | 190 | // Gather scaled press & release differences 191 | let pressScaled = attemptSummaries.map((s) => s.pressDifferenceScaled); 192 | let releaseScaled = attemptSummaries.map((s) => s.releaseDifferenceScaled); 193 | 194 | // Compute winsorized means (5%-95%) 195 | const pressMeanVal = winsorizedMean(pressScaled, 0.05, 0.95); 196 | const releaseMeanVal = winsorizedMean(releaseScaled, 0.05, 0.95); 197 | 198 | // Add small buffer of 7 ms 199 | let recommendedPressBase = Math.ceil(pressMeanVal + 7); 200 | let recommendedReleaseBase = Math.ceil(releaseMeanVal + 7); 201 | 202 | // 4. If user provided WPM, clamp to a safe threshold 203 | // Typical formula: average inter-keystroke time = 60000 / (5 * WPM) ms 204 | // Then pick 50% or so as a "safe" max for single-letter typing 205 | let computedIKIMs = null; // IKI = inter-keystroke interval 206 | let usedSafeMax = null; 207 | const numericWpm = parseFloat(wpm) || 0; 208 | if (numericWpm > 0) { 209 | const averageMsPerKeystroke = 60000 / (5 * numericWpm); 210 | // e.g. 100 WPM => 60000/(5*100)=60000/500=120ms per keystroke on average 211 | const safeMax = 0.5 * averageMsPerKeystroke; 212 | // 50% of that, to avoid accidental chords if they burst to a higher speed 213 | computedIKIMs = averageMsPerKeystroke; 214 | usedSafeMax = safeMax/2; 215 | // Final recommended press = min of chord-based vs. safe max / 2, because 2 keys= 2*press tolerance is timing 216 | recommendedPressBase = Math.min(recommendedPressBase, Math.floor(safeMax/2)); 217 | } 218 | 219 | // Final set 220 | setRecommendations({ 221 | pressMedian: pressMeanVal, // or rename key to pressMean if you prefer 222 | releaseMedian: releaseMeanVal, 223 | recommendedPress: recommendedPressBase, 224 | recommendedRelease: recommendedReleaseBase, 225 | averageMsKeystroke: computedIKIMs, 226 | safeMaxUsed: usedSafeMax, 227 | }); 228 | }; 229 | 230 | const handleReset = () => { 231 | setAttempts([]); 232 | setCurrentAttempt([]); 233 | setPressedKeys(new Set()); 234 | setAttemptSummaries([]); 235 | setTextFieldValue(''); 236 | setWpm('80'); 237 | setRecommendations({ 238 | pressMedian: 0, 239 | releaseMedian: 0, 240 | recommendedPress: 0, 241 | recommendedRelease: 0, 242 | }); 243 | }; 244 | 245 | return ( 246 | 247 | 248 | Chord Tolerance Recommender 249 | 250 | 251 | This tool helps you dial in suggested{' '} 252 | 253 | Press 254 | {' '} 255 | and{' '} 256 | 257 | Release 258 | {' '} 259 | tolerances for your CharaChorder. First, turn off chording on 260 | your device. Then pick a few chords of varying lengths (e.g. 3-key, 261 | 4-key) and chord each chord multiple times in the box below. 262 | As you chord, the recommended values will be computed and shown. 263 | 264 | 265 | 266 | 274 | setWpm(e.target.value)} 280 | sx={{ width: 200 }} 281 | /> 282 | 285 | 286 | 287 | 288 | Chords recorded: {attempts.length} 289 | 290 | 291 | {attemptSummaries.length > 0 && recommendations.recommendedPress > 0 && ( 292 | 293 | Recommendations 294 | 295 | 296 | Press Tolerance: {recommendations.recommendedPress} ms 297 | 298 | 299 | Release Tolerance: {recommendations.recommendedRelease} ms 300 | 301 | 302 | 303 | 304 | Note:{' '} 305 | Because of chord scaling, if you press 2 keys with a Press Tolerance of{' '} 306 | {recommendations.recommendedPress} ms, the actual chord window is{' '} 307 | {2 * recommendations.recommendedPress} ms total.{' '} 308 | 309 | {recommendations.averageMsKeystroke && ( 310 | <> 311 | We computed your average inter-keystroke interval at about{' '} 312 | {recommendations.averageMsKeystroke.toFixed(1)} ms based on {wpm} WPM, 313 | then assumed you may burst to 2× that speed. Combining that with a potential 2 keys overlapping, we 314 | clamped your Press Tolerance to 315 | {recommendations.safeMaxUsed?.toFixed(1)} ms 316 | to minimize accidental chords when typing quickly. 317 | 318 | )} 319 | 320 | 321 | 322 | )} 323 | 324 | {attemptSummaries.length > 0 && ( 325 | 326 | Chord Details 327 | 328 | 329 | 330 | Attempt # 331 | Press Diff (ms) 332 | Release Diff (ms) 333 | Keys Pressed 334 | Press Diff (scaled, ms/key) 335 | Release Diff (scaled, ms/key) 336 | 337 | 338 | 339 | {attemptSummaries.map((s, idx) => ( 340 | 341 | {idx + 1} 342 | {s.pressDifference.toFixed(2)} 343 | {s.releaseDifference.toFixed(2)} 344 | {s.keysPressed} 345 | {s.pressDifferenceScaled.toFixed(2)} 346 | {s.releaseDifferenceScaled.toFixed(2)} 347 | 348 | ))} 349 | 350 |
351 |
352 | )} 353 | {attemptSummaries.length > 0 && recommendations.recommendedPress > 0 && ( 354 | 355 | 356 | 357 | Press mean (scaled): {recommendations.pressMedian.toFixed(2)} ms/key 358 | 359 | 360 | Release mean (scaled): {recommendations.releaseMedian.toFixed(2)} ms/key 361 | 362 | 363 | 364 | )} 365 |
366 | ); 367 | }; 368 | 369 | export default ToleranceRecommender; -------------------------------------------------------------------------------- /src/functions/anagramWorker.js: -------------------------------------------------------------------------------- 1 | function countWordOccurrences(textInput, word) { 2 | var words = getCleanWordsFromString(textInput); 3 | let count = 0; 4 | for (let i = 0; i < words.length; i++) { 5 | if (words[i] === word) { 6 | count++; 7 | } 8 | } 9 | return count; 10 | } 11 | 12 | function getCleanWordsFromString(inputString) { 13 | // Replace all new line and tab characters with a space character, and remove consecutive spaces 14 | const textWithSpaces = inputString.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' '); 15 | 16 | // Split the text into an array of words 17 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, '')); 18 | const words = origWords 19 | // Remove empty strings from the array 20 | .filter(word => word.trim().length > 0) 21 | // Convert all words to lower case 22 | .map(word => word.toLowerCase()); 23 | return words; 24 | } 25 | 26 | function findPartialAnagrams(inputString) { 27 | const words = getCleanWordsFromString(inputString); 28 | const result = []; 29 | const uniqueWords = [...new Set(words)]; 30 | 31 | // Calculate the total number of iterations (for progress calculation) 32 | const totalIterations = uniqueWords.length * (uniqueWords.length - 1); 33 | console.time("Analyzing Time"); 34 | console.log("Expected total iterations:", totalIterations); 35 | let currentIteration = 0; 36 | 37 | for (let i = 0; i < uniqueWords.length; i++) { 38 | // For each word, compare it to the other words in the array 39 | for (let j = 0; j < uniqueWords.length; j++) { 40 | // Skip the current word if it is being compared to itself 41 | if (i !== j) { 42 | // Increment current iteration and calculate progress 43 | currentIteration++; 44 | 45 | const progress = (currentIteration / totalIterations) * 100; 46 | if (currentIteration % 100000 === 0) { 47 | //console.log(progress) 48 | postMessage({ type: 'progress', progress }); 49 | } 50 | 51 | // Check if the words are equal 52 | if (uniqueWords[i] !== uniqueWords[j]) { 53 | // If they are not equal, check if they are partial anagrams 54 | const uniqueLetters1 = [...new Set(uniqueWords[i].split(''))]; 55 | const uniqueLetters2 = [...new Set(uniqueWords[j].split(''))]; 56 | 57 | // Check if each unique letter in one word appears in the other word 58 | if (uniqueLetters1.every(letter => uniqueLetters2.includes(letter)) && uniqueLetters2.every(letter => uniqueLetters1.includes(letter))) { 59 | // Check if the pair has already been added to the result array 60 | if (!result.some(pair => pair.includes(uniqueWords[i]) && pair.includes(uniqueWords[j]))) { 61 | if (result.length === 0) { 62 | result.push([uniqueWords[i], uniqueWords[j]]); 63 | } else { 64 | let found = false; 65 | for (let k = 0; k < result.length; k++) { 66 | if (result[k].includes(uniqueWords[i]) || result[k].includes(uniqueWords[j])) { 67 | if (!(result[k].includes(uniqueWords[i]))) { 68 | result[k].push(uniqueWords[i]); 69 | } 70 | if (!(result[k].includes(uniqueWords[j]))) { 71 | result[k].push(uniqueWords[j]); 72 | } 73 | found = true; 74 | break; 75 | } 76 | } 77 | if (!found) { 78 | result.push([uniqueWords[i], uniqueWords[j]]); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | console.timeEnd("Analyzing Time"); 88 | console.time("Sorting Time"); // Start the timer with label "Sorting Time" 89 | const resultWithCounts = []; 90 | 91 | for (const group of result) { 92 | const groupWithCounts = group.map(word => { 93 | return { word: word, count: countWordOccurrences(inputString, word) }; 94 | }); 95 | resultWithCounts.push(groupWithCounts); 96 | } 97 | 98 | // Sort the result array by the largest list of partial anagrams first 99 | resultWithCounts.sort((a, b) => b.length - a.length); 100 | console.timeEnd("Sorting Time"); // End the timer with label "Sorting Time" 101 | 102 | return resultWithCounts; 103 | } 104 | 105 | export function anagramWorkerFunction(e) { 106 | if (e.data.type === 'computeAnagrams') { 107 | postMessage({ type: 'progress', progress: 0 }); // Initial progress message 108 | const result = findPartialAnagrams(e.data.input); 109 | postMessage({ type: 'result', result }); // Final result 110 | } 111 | } 112 | 113 | // eslint-disable-next-line no-restricted-globals 114 | self.onmessage = anagramWorkerFunction; -------------------------------------------------------------------------------- /src/functions/chordGenerationWorker.js: -------------------------------------------------------------------------------- 1 | function generateChords(words, sliderValue, checkboxStates, chordLibrary) { 2 | const MIN_WORD_LENGTH = 2; 3 | const ALT_KEYS = ['LEFT_ALT', 'RIGHT_ALT']; 4 | const KEY_MIRROR_MAP_L = { 5 | ",": ";", 6 | "u": "s", 7 | "'": "y", 8 | ".": "j", 9 | "o": "n", 10 | "i": "l", 11 | "e": "t", 12 | "SPACE": "SPACE", 13 | "BKSP": "ENTER", 14 | "r": "a", 15 | "v": "p", 16 | "m": "h", 17 | "c": "d", 18 | "k": "f", 19 | "z": "q", 20 | "w": "b", 21 | "g": null, 22 | "x": null 23 | }; 24 | const KEY_MIRROR_MAP_R = Object.fromEntries(Object.entries(KEY_MIRROR_MAP_L).map(([key, value]) => [value, key])); 25 | const KEY_FINGER_MAP = { 26 | "LH_PINKY": ['LEFT_ALT'], 27 | "LH_RING_1": [',', 'u', "'"], 28 | "LH_MID_1": ['.', 'o', 'i'], 29 | "LH_INDEX": ['e', 'r', 'SPACE', 'BKSP'], 30 | "LH_THUMB_1": ['m', 'v', 'k', 'c'], 31 | "LH_THUMB_2": ['g', 'z', 'w'], 32 | "RH_THUMB_2": ['x', 'b', 'q', 'DUP'], 33 | "RH_THUMB_1": ['p', 'f', 'd', 'h'], 34 | "RH_INDEX": ['a', 't', 'SPACE', 'ENTER'], 35 | "RH_MID_1": ['l', 'n', 'j'], 36 | "RH_RING_1": ['y', 's', ';'], 37 | "RH_PINKY": ['RIGHT_ALT'] 38 | }; 39 | const CONFLICTING_FINGER_GROUPS_DOUBLE = { 40 | "LH_PINKY": ['LEFT_ALT', 'LH_PINKY_3D'], 41 | "LH_RING_1": [',', 'u', "'", 'LH_RING_1_3D'], 42 | "LH_MID_1": ['.', 'o', 'i', 'LH_MID_1_3D'], 43 | "LH_INDEX": ['e', 'r', 'LH_INDEX_3D', 'SPACE', 'BKSP'], 44 | "LH_THUMB": ['m', 'v', 'k', 'c', 'LH_THUMB_1_3D', 'g', 'z', 'w', 'LH_THUMB_1_3D'], 45 | "RH_THUMB": ['x', 'b', 'q', 'DUP', 'RH_THUMB_1_3D', 'p', 'f', 'd', 'h', 'RH_THUMB_2_3D'], 46 | "RH_INDEX": ['a', 't', 'RH_INDEX_3D', 'SPACE', 'ENTER'], 47 | "RH_MID_1": ['l', 'n', 'j', 'RH_MID_1_3D'], 48 | "RH_RING_1": ['y', 's', ';', 'RH_RING_1_3D'], 49 | "RH_PINKY": ['RIGHT_ALT', 'RH_PINKY_3D'] 50 | }; 51 | const CONFLICTING_FINGER_GROUPS_TRIPLE = { 52 | "group_1": ['a', 'n', 'y'], 53 | "group_2": ['r', 'o', "'"] 54 | } 55 | 56 | const UNUSABLE_CHORDS = { 57 | "impulse_chord": ['DUP', 'i'] 58 | }; 59 | 60 | const settings = { 61 | useDupKey: checkboxStates.useDupKey, 62 | useMirroredKeys: checkboxStates.useMirroredKeys, 63 | useAltKeys: checkboxStates.useAltKeys, 64 | use3dKeys: checkboxStates.use3dKeys, 65 | sliderValue: sliderValue, 66 | chordLibrary: chordLibrary 67 | }; 68 | 69 | const onlyChars = true; 70 | const useDupKey = settings.useDupKey; 71 | const useMirroredKeys = settings.useMirroredKeys; 72 | const useAltKeys = settings.useAltKeys; 73 | const use3dKeys = settings.use3dKeys; 74 | const minChordLength = settings.sliderValue[0]; 75 | const maxChordLength = settings.sliderValue[1]; 76 | let usedChords = {}; 77 | let uploadedChords = new Map(); 78 | let localWords = [...new Set(words.map(word => word.toLowerCase()))]; 79 | 80 | const CHORD_GENERATORS_MAP = { 81 | 'onlyCharsGenerator': onlyCharsGenerator, 82 | 'useMirroredKeysGenerator': useMirroredKeysGenerator, 83 | 'useAltKeysGenerator': useAltKeysGenerator, 84 | 'use3dKeysGenerator': use3dKeysGenerator 85 | }; 86 | 87 | const SETTINGS_MAP = { 88 | 'onlyChars': onlyChars, 89 | 'useMirroredKeys': useMirroredKeys, 90 | 'useAltKeys': useAltKeys, 91 | 'use3dKeys': use3dKeys 92 | }; 93 | 94 | function wordsList() { 95 | return localWords.filter(word => word.length >= MIN_WORD_LENGTH); 96 | } 97 | 98 | function loadUploadedChords() { 99 | return new Promise((resolve, reject) => { 100 | try { 101 | if (!chordLibrary || !Array.isArray(chordLibrary)) { 102 | console.warn('chordLibrary is not properly initialized.'); 103 | resolve(); 104 | return; 105 | } 106 | 107 | chordLibrary.forEach((entry) => { 108 | if (entry.chordInput && entry.chordOutput) { 109 | const chord = entry.chordInput.trim().split(' + '); 110 | const word = entry.chordOutput.trim(); 111 | 112 | if (uploadedChords.has(word)) { 113 | uploadedChords.get(word).push(chord); 114 | } else { 115 | uploadedChords.set(word, [chord]); 116 | } 117 | } 118 | }); 119 | resolve(); 120 | } catch (error) { 121 | reject(error); 122 | } 123 | }); 124 | } 125 | 126 | function calculateChord(chars) { 127 | for (const generatorKey of Object.keys(CHORD_GENERATORS_MAP)) { 128 | const option = generatorKey.replace('Generator', ''); 129 | if (SETTINGS_MAP[option]) { 130 | const chord = CHORD_GENERATORS_MAP[generatorKey](chars); // function call here 131 | if (chord) { 132 | const reversedChord = chord.slice().sort().reverse(); // Ensuring slice() so as not to mutate the original array 133 | return reversedChord; 134 | } 135 | } 136 | } 137 | return null; 138 | } 139 | 140 | function getChars(word) { 141 | const chars = word.split("").filter(str => str !== " "); 142 | let uniq_chars = [...new Set(chars)]; 143 | if (uniq_chars.length < chars.length && useDupKey) { 144 | uniq_chars = [...new Set(chars), "DUP"] 145 | } 146 | const validChars = Object.values(KEY_FINGER_MAP).flat(); 147 | 148 | return uniq_chars.filter(char => validChars.includes(char)); 149 | } 150 | 151 | function assignChord(word, chord) { 152 | usedChords[word] = chord; 153 | } 154 | 155 | function onlyCharsGenerator(chars) { 156 | for (const chord of allCombinations(chars)) { 157 | if (validChord(chord)) return chord; 158 | } 159 | } 160 | 161 | function useMirroredKeysGenerator(chars) { 162 | for (const chord of allCombinations([...new Set([...chars, ...mirrorKeys(chars)])])) { 163 | if (validChord(chord)) return chord; 164 | } 165 | } 166 | 167 | function use3dKeysGenerator(chars) { 168 | for (const chord of allCombinations([...new Set([...chars, ...threeDKeys(chars)])])) { 169 | if (validChord(chord)) return chord; 170 | } 171 | } 172 | 173 | function useAltKeysGenerator(chars) { 174 | for (const chord of allCombinations([...new Set([...chars, ...ALT_KEYS])])) { 175 | if (validChord(chord)) return chord; 176 | } 177 | } 178 | 179 | function validChord(chord) { 180 | return !fingerConflict(chord) && !usedChord(chord) && !uploadedChord(chord); 181 | } 182 | 183 | const powerSetCache = {}; 184 | 185 | function powerSet(chars) { 186 | const cacheKey = chars.join(','); 187 | if (powerSetCache[cacheKey]) return powerSetCache[cacheKey]; 188 | 189 | const result = [[]]; 190 | for (const value of chars) { 191 | const length = result.length; 192 | for (let i = 0; i < length; i++) { 193 | const subset = result[i]; 194 | result.push(subset.concat(value)); 195 | } 196 | } 197 | powerSetCache[cacheKey] = result; 198 | return result; 199 | } 200 | 201 | function allCombinations(chars) { 202 | return powerSet(chars) 203 | .filter(subset => subset.length >= minChordLength && subset.length <= maxChordLength) 204 | .sort((a, b) => a.length - b.length); 205 | } 206 | 207 | function fingerConflict(chord) { 208 | const sortedChord = [...chord].sort(); 209 | if (hasDuplicates(chord)) return true; 210 | if (Object.values(CONFLICTING_FINGER_GROUPS_DOUBLE).some((fingerKeys) => fingerKeys.filter((key) => chord.includes(key)).length > 1)) return true; 211 | if (Object.values(CONFLICTING_FINGER_GROUPS_TRIPLE).some((fingerKeys) => fingerKeys.filter((key) => chord.includes(key)).length > 2)) return true; 212 | if (Object.values(UNUSABLE_CHORDS).some((fingerKeys) => JSON.stringify([...fingerKeys].sort()) === JSON.stringify(sortedChord))) return true; 213 | 214 | return false; 215 | } 216 | 217 | function hasDuplicates(chord) { 218 | return chord.length > new Set(chord).size; 219 | } 220 | 221 | function usedChord(chord) { 222 | const sortedChord = [...chord].sort(); 223 | return Object.values(usedChords).some(usedChord => { 224 | const sortedUsedChord = [...usedChord].sort(); 225 | return JSON.stringify(sortedUsedChord) === JSON.stringify(sortedChord); 226 | }); 227 | } 228 | 229 | function uploadedChord(chord) { 230 | const sortedChord = [...chord].sort(); 231 | 232 | for (const [, chords] of uploadedChords.entries()) { 233 | for (const uploadedChord of chords) { 234 | const sortedUploadedChord = [...uploadedChord].sort(); 235 | if (JSON.stringify(sortedUploadedChord) === JSON.stringify(sortedChord)) { 236 | return true; 237 | } 238 | } 239 | } 240 | return false; 241 | } 242 | 243 | function mirrorKeys(chord) { 244 | return chord.map(char => KEY_MIRROR_MAP_L[char] || KEY_MIRROR_MAP_R[char]).filter(Boolean); 245 | } 246 | 247 | function threeDKeys(chord) { 248 | return chord.map(char => getThreeDKey(char)).filter(Boolean); 249 | } 250 | 251 | function getThreeDKey(char) { 252 | for (const [finger, chars] of Object.entries(KEY_FINGER_MAP)) { 253 | if (chars.includes(char)) return `${finger}_3D`; 254 | } 255 | return null; 256 | } 257 | 258 | function generate() { 259 | loadUploadedChords(); 260 | const totalWords = wordsList().length; 261 | 262 | let currentIteration = 0 263 | let skippedWords = 0 264 | const failedWords = []; 265 | 266 | for (let index = 0; index < totalWords; index++) { 267 | const word = wordsList()[index]; 268 | // Skip if the word is already in chordLibrary 269 | if (uploadedChords.has(word)) { 270 | //console.log(`Skipping '${word}' because it already in chordLibrary.`); 271 | skippedWords++; 272 | continue; 273 | } 274 | 275 | const chord = calculateChord(getChars(word)); 276 | if (chord) { 277 | assignChord(word, chord.sort().reverse()); 278 | } else { 279 | //console.log("Could not generate chord for", word); 280 | failedWords.push(word); 281 | } 282 | 283 | currentIteration++; 284 | 285 | const progress = (currentIteration / totalWords) * 100; 286 | postMessage({ type: 'progress', progress }); 287 | } 288 | return { 289 | usedChords: usedChords, 290 | skippedWordCount: skippedWords, 291 | failedWords: failedWords 292 | }; 293 | } 294 | 295 | let results = generate(); 296 | return results; 297 | } 298 | 299 | // eslint-disable-next-line no-restricted-globals 300 | self.onmessage = function (e) { 301 | if (e.data.type === 'generateChords') { 302 | postMessage({ type: 'progress', progress: 0 }); // Initial progress message 303 | const result = generateChords(e.data.wordsArray, e.data.sliderValue, e.data.checkboxStates, e.data.chordLibrary); 304 | postMessage({ type: 'result', result }); // Final result 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/functions/createCsv.js: -------------------------------------------------------------------------------- 1 | import Papa from 'papaparse'; 2 | 3 | export default function createCsv(data, filename = 'data.csv', includeHeader = true) { 4 | const config = { 5 | header: includeHeader 6 | }; 7 | const csv = Papa.unparse(data, config); 8 | 9 | // Create a Blob from the CSV string 10 | const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); 11 | 12 | // Create a hidden anchor element 13 | const link = document.createElement('a'); 14 | const url = URL.createObjectURL(blob); 15 | 16 | link.setAttribute('href', url); 17 | link.setAttribute('download', filename); 18 | link.style.visibility = 'hidden'; 19 | 20 | // Append, trigger the download, and remove the anchor element 21 | document.body.appendChild(link); 22 | link.click(); 23 | document.body.removeChild(link); 24 | }; -------------------------------------------------------------------------------- /src/functions/wordAnalysis.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function calculateBigrams(text) { 4 | var words = text.split(' '); 5 | var wordCount = words.length; 6 | 7 | // Use the filter method and the indexOf method to remove any duplicates 8 | var uniqueWords = words.filter(function (word, index) { 9 | return words.indexOf(word) === index; 10 | }); 11 | 12 | // Create a list of bigrams by iterating over each word in the array 13 | var bigrams = []; 14 | for (var i = 0; i < words.length; i++) { 15 | // Remove any leading or trailing spaces from the word 16 | var word = words[i].trim(); 17 | 18 | // Skip over any empty words (which can happen if the text has multiple consecutive spaces) 19 | if (word.length >= 2) { 20 | // Check for line breaks in the word 21 | if (word.indexOf('\n') === -1) { 22 | for (var j = 0; j < word.length - 1; j++) { 23 | var lowered = word.toLowerCase() 24 | var bigram = lowered.substring(j, j + 2); 25 | const [letter1, letter2] = bigram.split(''); 26 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) { 27 | // If either letter is not a lowercase letter, skip this iteration 28 | continue; 29 | } 30 | // sort the characters in the bigram so that permutations are counted together 31 | bigram = bigram.split('').sort().join(''); 32 | var reversedbigram = bigram.split('').reverse().join(''); 33 | bigram = bigram + " | " + reversedbigram; 34 | bigrams.push(bigram); 35 | } 36 | } 37 | } 38 | } 39 | 40 | // create an object to hold the frequency of each bigram 41 | var bigramCounts = {}; 42 | for (let i = 0; i < bigrams.length; i++) { 43 | let bigram = bigrams[i]; 44 | if (bigram in bigramCounts) { 45 | bigramCounts[bigram]++; 46 | } else { 47 | bigramCounts[bigram] = 1; 48 | } 49 | } 50 | 51 | return [bigramCounts, wordCount, uniqueWords.length]; 52 | } 53 | 54 | export function createFrequencyTable(bigramCounts) { 55 | // Create a matrix of zeroes with 26 rows and 26 columns 56 | // (assuming you only have lowercase letters in your input) 57 | const matrix = Array(26) 58 | .fill() 59 | .map(() => Array(26).fill(0)); 60 | 61 | const normalizedMatrix = Array(26) 62 | .fill() 63 | .map(() => Array(26).fill(0)); 64 | 65 | // Loop through each key-value pair in the input object 66 | for (const [key, value] of Object.entries(bigramCounts)) { 67 | // Split the key into two letters 68 | const [letter1, letter2] = key.split(" ")[0].split(""); 69 | 70 | // Check if either letter1 or letter2 is not a lowercase letter 71 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) { 72 | // If either letter is not a lowercase letter, skip this iteration 73 | continue; 74 | } 75 | 76 | // Convert the letters to their ASCII codes (a = 97, b = 98, etc.) 77 | const ascii1 = letter1.charCodeAt(0) - 97; 78 | const ascii2 = letter2.charCodeAt(0) - 97; 79 | 80 | matrix[ascii1][ascii2] = value; 81 | matrix[ascii2][ascii1] = value; 82 | 83 | // Normalize the value to be in the range 0-255 84 | const normalizedValue = (value / Math.max(...Object.values(bigramCounts))) * 255 85 | 86 | // Update the matrix with the value from the input object 87 | normalizedMatrix[ascii1][ascii2] = normalizedValue; 88 | normalizedMatrix[ascii2][ascii1] = normalizedValue; // (assuming you want the matrix to be symmetrical) 89 | } 90 | return [matrix, normalizedMatrix] 91 | } 92 | 93 | async function calcStats(text, chords, minReps, lemmatizeChecked) { 94 | const counts = await findUniqueWords(chords, text, lemmatizeChecked); 95 | return [counts.sortedWords, counts.uniqueWordCount, counts.chordWordCount]; 96 | } 97 | 98 | export function findMissingChords(textToAnalyze, chordLibrary) { 99 | const chords = new Set(); 100 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput)); 101 | return calcStats(textToAnalyze, chords, 5, false); 102 | } 103 | 104 | async function processWords(words, lemmatize) { 105 | for (let i = 0; i < words.length; i++) { 106 | words[i] = words[i].toLowerCase().replace(/[^\w\s]/g, ''); 107 | if (lemmatize) { 108 | // const doc = nlp(words[i]) 109 | // doc.verbs().toInfinitive() 110 | // doc.nouns().toSingular() 111 | // const lemma = doc.out('text') 112 | // words[i] = lemma; 113 | } 114 | } 115 | return words; 116 | } 117 | 118 | async function findUniqueWords(chords, text, lemmatize) { 119 | // Split the text into an array of words 120 | var words = text.split(/\s+/); 121 | words = await processWords(words, lemmatize); 122 | 123 | var wordCounts = {}; 124 | var uniqueWordCount = 0; 125 | var chordWordCount = 0; 126 | var countedChords = {}; 127 | 128 | // Count the number of times each word appears in the text 129 | for (var i = 0; i < words.length; i++) { 130 | var word = words[i].trim(); 131 | if (word === "") { 132 | continue; 133 | } 134 | if (word.length > 1) { 135 | if (!(word in wordCounts)) { 136 | wordCounts[word] = 1; 137 | uniqueWordCount++; 138 | } else { 139 | wordCounts[word]++; 140 | } 141 | } 142 | } 143 | 144 | // Create a dictionary of words that do not appear in the chords set 145 | var sortedWords = {}; 146 | for (let i = 0; i < words.length; i++) { 147 | let word = words[i].trim(); 148 | if (word === "") { 149 | continue; 150 | } 151 | if (word.length > 1) { 152 | if (!chords.has(word)) { 153 | if (!(word in sortedWords)) { 154 | sortedWords[word] = 1; 155 | } else { 156 | sortedWords[word]++; 157 | } 158 | } else { 159 | if (!(word in countedChords)) { 160 | countedChords[word] = true; 161 | chordWordCount++; 162 | } 163 | } 164 | } 165 | } 166 | 167 | var descSortedWords = Object.entries(sortedWords).sort((a, b) => b[1]*b[0].length - a[1]*a[0].length); 168 | return { 169 | sortedWords: descSortedWords, 170 | uniqueWordCount: uniqueWordCount, 171 | chordWordCount: chordWordCount 172 | }; 173 | } 174 | 175 | export function getPhraseFrequency(text, phraseLength, minRepetitions, chordLibrary) { 176 | const chords = new Set(); 177 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput)); 178 | 179 | // Replace all new line and tab characters with a space character, and remove consecutive spaces 180 | const textWithSpaces = text.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' '); 181 | 182 | // Split the text into an array of words 183 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, '')); 184 | const words = origWords 185 | // Remove empty strings from the array 186 | .filter(word => word.trim().length > 0) 187 | // Convert all words to lower case 188 | .map(word => word.toLowerCase()); 189 | 190 | 191 | // Create a dictionary to store the phrases and their frequency 192 | const phraseFrequency = {}; 193 | 194 | // Iterate over the words array and add each phrase to the dictionary 195 | for (let i = 0; i < words.length; i++) { 196 | for (let j = 2; j <= phraseLength; j++) { 197 | // Get the current phrase by joining the next `j` words with a space character 198 | const phrase = words.slice(i, i + j).join(' '); 199 | // Check if the phrase fits within the bounds of the `words` array 200 | if (i + j <= words.length) { 201 | // Split the phrase into a list of words 202 | const phraseWords = phrase.split(' '); 203 | // Check if the phrase contains at least two words 204 | if (phraseWords.length >= 2) { 205 | // If the phrase is already in the dictionary, increment its frequency. Otherwise, add it to the dictionary with a frequency of 1. 206 | if (phrase in phraseFrequency) { 207 | phraseFrequency[phrase]++; 208 | } else { 209 | phraseFrequency[phrase] = 1; 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | // Filter the sorted phrase frequency dictionary by the minimum number of repetitions 217 | const filteredPhraseFrequency = {}; 218 | Object.keys(phraseFrequency).forEach(phrase => { 219 | if (phraseFrequency[phrase] >= minRepetitions) { 220 | filteredPhraseFrequency[phrase] = phraseFrequency[phrase]; 221 | } 222 | }); 223 | 224 | // Remove entries from the filtered phrase frequency object that are already in the chords set 225 | const lowerCaseChords = new Set(Array.from(chords).map(phrase => phrase.trim().toLowerCase())); 226 | const filteredPhraseFrequencyWithoutChords = Object.keys(filteredPhraseFrequency) 227 | .filter(phrase => !lowerCaseChords.has(phrase)) 228 | .reduce((obj, phrase) => { 229 | obj[phrase] = filteredPhraseFrequency[phrase]; 230 | return obj; 231 | }, {}); 232 | 233 | let sortableArray = Object.entries(filteredPhraseFrequencyWithoutChords); 234 | sortableArray.sort((a, b) => { 235 | const scoreA = a[0].length * a[1]; 236 | const scoreB = b[0].length * b[1]; 237 | return scoreB - scoreA; 238 | }); 239 | 240 | return sortableArray; 241 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import { ThemeProvider } from '@mui/material/styles'; 5 | import App from './App'; 6 | import theme from './theme'; 7 | 8 | const rootElement = document.getElementById('root'); 9 | const root = ReactDOM.createRoot(rootElement); 10 | 11 | root.render( 12 | 13 | 14 | 15 | , 16 | ); -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/CCXDebugging.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Typography, Button, TextField } from '@mui/material'; 3 | import List from '@mui/material/List'; 4 | import ListItem from '@mui/material/ListItem'; 5 | import { Snackbar, Alert } from '@mui/material'; 6 | 7 | function CCX() { 8 | const [port, setPort] = useState(null); 9 | const [writer, setWriter] = useState(null); 10 | const [isTesting, setIsTesting] = useState(false); 11 | const [receivedData, setReceivedData] = useState(""); 12 | const [openSnackbar, setOpenSnackbar] = useState(false); 13 | const [supportsSerial, setSupportsSerial] = useState(null); 14 | 15 | const handleOpenSnackbar = () => { 16 | setOpenSnackbar(true); 17 | }; 18 | 19 | const handleCloseSnackbar = () => { 20 | setOpenSnackbar(false); 21 | }; 22 | 23 | useEffect(() => { 24 | if (port !== null) { 25 | listenToPort(); 26 | } 27 | }, [port]); 28 | 29 | useEffect(() => { 30 | if ('serial' in navigator) { 31 | setSupportsSerial(true) 32 | } 33 | else { 34 | setSupportsSerial(false) 35 | } 36 | }) 37 | 38 | const connectDevice = async () => { 39 | try { 40 | const newPort = await navigator.serial.requestPort(); 41 | await newPort.open({ baudRate: 115200 }); 42 | 43 | const textEncoder = new TextEncoderStream(); 44 | const writableStreamClosed = textEncoder.readable.pipeTo(newPort.writable); 45 | const newWriter = textEncoder.writable.getWriter(); 46 | 47 | setWriter(newWriter); 48 | setPort(newPort); 49 | } catch { 50 | alert("Serial Connection Failed"); 51 | } 52 | }; 53 | 54 | const listenToPort = async () => { 55 | if (port === null) return; 56 | 57 | const textDecoder = new TextDecoderStream(); 58 | const readableStreamClosed = port.readable.pipeTo(textDecoder.writable); 59 | const reader = textDecoder.readable.getReader(); 60 | 61 | while (true) { 62 | const { value, done } = await reader.read(); 63 | console.log(value); 64 | 65 | if (done) { 66 | reader.releaseLock(); 67 | break; 68 | } 69 | setReceivedData((prev) => prev + value); 70 | } 71 | }; 72 | 73 | const toggleTest = async () => { 74 | if (!isTesting) { 75 | if (writer) { 76 | await writer.write("VERSION\r\n"); 77 | await writer.write("VAR B2 C1 1\r\n"); 78 | } 79 | setIsTesting(true); 80 | } else { 81 | if (writer) { 82 | await writer.write("VAR B2 C1 0\r\n"); 83 | } 84 | setIsTesting(false); 85 | copyToClipboard() 86 | } 87 | }; 88 | 89 | const copyToClipboard = () => { 90 | if (navigator.clipboard) { 91 | navigator.clipboard.writeText(receivedData); 92 | } else { 93 | // Fallback for older browsers 94 | const textArea = document.createElement("textarea"); 95 | textArea.value = receivedData; 96 | document.body.appendChild(textArea); 97 | textArea.focus(); 98 | textArea.select(); 99 | document.execCommand('copy'); 100 | document.body.removeChild(textArea); 101 | } 102 | handleOpenSnackbar(); 103 | }; 104 | 105 | return ( 106 |
107 | CharaChorder X Debugging 108 | {supportsSerial === null ? ( 109 | "Checking for Serial API support..." 110 | ) : supportsSerial ? ( 111 | <> 112 | 113 | In order to help the CharaChorder team debug why your 114 | keyboard may not be working with the CCX, please perform the following 115 | steps and then copy and send the output once you are done. 116 | Read all instructions first before starting with step 1. 117 | 118 | 119 | 120 | 0: Make sure your CCX is updated to CCOS 1.1.3 or a later version.  Click here for instructions on how to update your device. 121 | 122 | 123 | 1: Unplug your keyboard from the CCX and make sure 124 | the CCX is plugged into your computer. 125 | 126 | 127 | 2: Click "Connect" and use Chrome's serial connection to 128 | choose your CCX. Click "Start Test" (you should see a 129 | "VERSION" and "VAR") line appear in the Serial Data box. 130 | 131 | 132 | 3: Plug in your keyboard to the CCX and wait a 133 | couple of seconds before moving to the next step. 134 | 135 | 4: Press and release the letter "a". 136 | 137 | 5: Next, press and hold the letter "a", press and hold the letter "s", and keep 138 | adding one letter at a time until you are pressing and holding all keys in "asdfjkl;" and 139 | then release all at once. Make sure to press the keys in order. 140 | 141 | 6: Press and release the "Left Shift" key. 142 | 143 | 7: Press the "Left Shift" key, press "Left Alt" key, 144 | and then release both. 145 | 146 | 147 | 8: Press the "a" key, press "Left Shift" key, and 148 | then release both. 149 | 150 | 151 | 9: Click Stop Test. The results are copied to your clipboard. You can 152 | send this to your CharaChorder support rep. 153 | 154 | 155 | 156 | 159 | Serial Data: 160 | 168 | 174 | 175 | Copied to clipboard! 176 | 177 | 178 | What is this test doing? 179 | 180 | When keyboard manufacturers make a keyboard that is plug and play, 181 | they have a few formatting options when it comes time to actually send what 182 | you, the user, type on your keyboard to your computer. There 183 | is a standard format that most keyboards use and the 184 | CCX works out of the box with that one (boot protocol); however, 185 | there are others that may be non-standard and when this 186 | happens, your CCX may not work properly. 187 | This test turns on a debugging command and then records 188 | the outputs from your device as you press various keys. 189 | The codes you see in the box are the codes that your 190 | keyboard is sending to the CharaChorder X and then the 191 | CharaChorder X has to interpet what it is, do its own 192 | processing/magic of chording and then send on to the 193 | computer the right text. If your keyboard isn't sending, 194 | for example, "KYBRPT 00 0000040000000000" for the 195 | letter "a", this test will let CharaChorder support see what it IS sending 196 | and then they may be able to infer/make a game plan to support 197 | what your keyboard is sending. 198 | 199 | 200 | ) : ( 201 | 202 | Your browser does not support the Serial API. Please use 203 | Google Chrome, Microsoft Edge, or another browser 204 | that supports the Serial API in order to use this tool. 205 | 206 | )} 207 |
208 | ); 209 | } 210 | 211 | export default CCX; -------------------------------------------------------------------------------- /src/pages/ChordTools.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useLocation, useNavigate } from 'react-router-dom'; 4 | import Tabs from '@mui/material/Tabs'; 5 | import Tab from '@mui/material/Tab'; 6 | import Typography from '@mui/material/Typography'; 7 | import Box from '@mui/material/Box'; 8 | import ChordDiagnostic from '../components/ChordDiagnostic'; 9 | import ChordStatistics from '../components/ChordStatistics'; 10 | import CC1ChordGenerator from '../components/CC1ChordGenerator'; 11 | import ChordViewer from '../components/ChordViewer'; 12 | import ToleranceRecommender from '../components/ToleranceRecommender'; 13 | 14 | function CustomTabPanel(props) { 15 | const { children, value, index, ...other } = props; 16 | 17 | return ( 18 | 31 | ); 32 | } 33 | 34 | CustomTabPanel.propTypes = { 35 | children: PropTypes.node, 36 | index: PropTypes.number.isRequired, 37 | value: PropTypes.number.isRequired, 38 | }; 39 | 40 | function a11yProps(index) { 41 | return { 42 | id: `simple-tab-${index}`, 43 | 'aria-controls': `simple-tabpanel-${index}`, 44 | }; 45 | } 46 | 47 | function ChordTools({ chordLibrary, setChordLibrary }) { 48 | const navigate = useNavigate(); 49 | const location = useLocation(); 50 | 51 | // Local state to keep track of current tab 52 | const [value, setValue] = useState(0); 53 | 54 | // On initial render or if location.search changes, read ?tab=xxx 55 | useEffect(() => { 56 | const searchParams = new URLSearchParams(location.search); 57 | const tabParam = searchParams.get('tab'); 58 | 59 | if (tabParam !== null) { 60 | const parsedTab = parseInt(tabParam, 10); 61 | if (!isNaN(parsedTab)) { 62 | setValue(parsedTab); 63 | } 64 | } 65 | }, [location.search]); 66 | 67 | // When user clicks a different tab, update both the local state and the URL 68 | const handleChange = (event, newValue) => { 69 | setValue(newValue); 70 | navigate( 71 | { 72 | pathname: '/chord-tools', 73 | search: `?tab=${newValue}`, 74 | }, 75 | { replace: true } // avoids pushing new history entries 76 | ); 77 | }; 78 | 79 | return ( 80 |
81 | 82 | Chord Tools 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | ); 111 | } 112 | 113 | ChordTools.propTypes = { 114 | chordLibrary: PropTypes.array, 115 | setChordLibrary: PropTypes.func, 116 | }; 117 | 118 | export default ChordTools; -------------------------------------------------------------------------------- /src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | function HomePage() { 5 | return ( 6 |
7 | Welcome to CharaChorder Utilites! 8 | 9 | This site contains a collection of tools for CharaChorder users. 10 | 11 | 12 | Most of the chord and practice tools require you to upload your exported chord library from Dot I/O. You can do that in the top right hand corner. After you have done it once, you will only need to load it again if you have added chords. 13 | 14 | 15 | Explore each of the pages and report any issues on the GitHub Page. 16 | 17 | 18 | Disclaimer: This site is not affiliated, associated, authorized, endorsed by, or in any way officially connected with CharaChorder. The official CharaChorder website can be found at CharaChorder.com. 19 | 20 |
21 | ); 22 | } 23 | 24 | export default HomePage; -------------------------------------------------------------------------------- /src/pages/Practice.css: -------------------------------------------------------------------------------- 1 | .centerToRightContainer { 2 | position: relative; 3 | left: 25%; 4 | width: 50%; 5 | } -------------------------------------------------------------------------------- /src/pages/Practice.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useCallback } from 'react'; 2 | import { TextField, Typography, Grid, Slider, Button, Dialog, DialogContent, DialogActions} from '@mui/material'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import DeleteIcon from '@mui/icons-material/Delete'; 5 | 6 | import { DataGrid } from '@mui/x-data-grid'; 7 | import './Practice.css'; 8 | 9 | 10 | function Practice({ chordLibrary }) { 11 | const [targetChords, setTargetChords] = useState([]); 12 | const [userInput, setUserInput] = useState(""); 13 | const [correctInput, setCorrectInput] = useState(null); 14 | const [sliderKeyValue, setSliderKeyValue] = useState([2, 10]); 15 | const [chordIndexValue, setChordIndexValue] = useState([1,chordLibrary.length]); 16 | const [filteredChords, setFilteredChords] = useState([]); 17 | const [chordStats, setChordStats] = useState({}); 18 | const [shouldUpdateChords, setShouldUpdateChords] = useState(true); 19 | const [currentWordIndex, setCurrentWordIndex] = useState(0); 20 | const [timerSeconds, setTimerSeconds] = useState(1 * 60); 21 | const [customTime, setCustomTime] = useState(1); 22 | const [timerActive, setTimerActive] = useState(false); 23 | const [totalTimePracticed, setTotalTimePracticed] = useState(0); 24 | const [openDialog, setOpenDialog] = React.useState(false); 25 | 26 | const inputRef = useRef(null); 27 | 28 | const numberOfTargetChords = 5; 29 | const uncheckable_keys = [ 30 | 'bksp', 'deldel', ' ', 'enter', 'lflf', 'rtrt', 31 | 'esc', 'arrow_up', 'arrow_dn', 'arrow_left', 'arrow_rt', 32 | 'pageup', 'pagedown', 'scrolllock', 'capslock', 'numlock', 'f1', 33 | 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f11', 'f12' 34 | ]; 35 | const wordPattern = /\S+/g; 36 | 37 | 38 | const resetState = () => { 39 | setTargetChords([]); 40 | setUserInput(""); 41 | setCorrectInput(null); 42 | setSliderKeyValue([2, 10]); 43 | setFilteredChords([]); 44 | setChordStats({}); 45 | setShouldUpdateChords(true); 46 | setCurrentWordIndex(0); 47 | setTimerSeconds(customTime * 60); 48 | setTimerActive(false); 49 | if (inputRef.current) { 50 | inputRef.current.disabled = false; 51 | inputRef.current.focus(); 52 | } 53 | }; 54 | 55 | const handleKeySliderChange = (event, newValue) => { 56 | setSliderKeyValue(newValue); 57 | }; 58 | 59 | const handleChordIndexChange = (event, newValue) => { 60 | setChordIndexValue(newValue); 61 | } 62 | 63 | const updateTotalTimePracticed = (time) => { 64 | const newTotalTime = totalTimePracticed + time; 65 | setTotalTimePracticed(newTotalTime); 66 | localStorage.setItem('totalTimePracticed', newTotalTime); 67 | }; 68 | 69 | const handleInputChange = (e) => { 70 | if (!timerActive) { 71 | setTimerActive(true); 72 | } 73 | setUserInput(e.target.value); 74 | }; 75 | 76 | const handleCustomTimeChange = (e) => { 77 | const newTime = parseFloat(e.target.value); 78 | setCustomTime(newTime); 79 | setTimerSeconds(newTime * 60); 80 | }; 81 | 82 | const handleKeyDown = (e) => { 83 | if (targetChords?.[0]) { 84 | let newChordStats = { ...chordStats }; 85 | const firstChordOutput = targetChords[0].chordOutput; 86 | 87 | if (e.key === 'Enter' || e.key === ' ') { 88 | const userInputWords = userInput.match(wordPattern); 89 | const targetWords = firstChordOutput.match(wordPattern); 90 | 91 | if ( 92 | userInputWords && targetWords && 93 | userInputWords.join(' ') === targetWords[currentWordIndex] 94 | ) { 95 | if (currentWordIndex === targetWords.length - 1) { 96 | let newTargetChords = targetChords.slice(1); 97 | const randomIndex = Math.floor(Math.random() * filteredChords.length); 98 | newTargetChords.push(filteredChords[randomIndex]); 99 | setTargetChords(newTargetChords); 100 | setCurrentWordIndex(0); 101 | } else { 102 | setCurrentWordIndex(currentWordIndex + 1); 103 | } 104 | 105 | setUserInput(""); 106 | setCorrectInput(null); 107 | 108 | newChordStats[firstChordOutput] = newChordStats[firstChordOutput] || { attempt: 0, correct: 0 }; 109 | newChordStats[firstChordOutput].attempt++; 110 | newChordStats[firstChordOutput].correct++; 111 | 112 | } else { 113 | setCorrectInput(targetChords[0].chordInput); 114 | newChordStats[firstChordOutput] = newChordStats[firstChordOutput] || { attempt: 0, correct: 0 }; 115 | newChordStats[firstChordOutput].attempt++; 116 | } 117 | 118 | setChordStats(newChordStats); 119 | } 120 | } 121 | }; 122 | 123 | const isValidChord = (chord) => { 124 | if (!chord.chordInput) return false; 125 | 126 | const chordArray = chord.chordInput 127 | .split('+') 128 | .map(str => str.trim().toLowerCase()); 129 | 130 | return chordArray.every(input => !uncheckable_keys.includes(input)); 131 | }; 132 | 133 | const updateTargetChords = useCallback(() => { 134 | if (!shouldUpdateChords) return; 135 | if (filteredChords.length === 0) return; 136 | 137 | let newTargetChords = []; 138 | 139 | for (let i = 0; i < numberOfTargetChords; i++) { 140 | if (filteredChords.length === 0) break; 141 | 142 | const randomIndex = Math.floor(Math.random() * filteredChords.length); 143 | const chord = filteredChords?.[randomIndex]; 144 | if (!chord) break; 145 | 146 | newTargetChords.push(chord); 147 | } 148 | 149 | setTargetChords(newTargetChords); 150 | setShouldUpdateChords(false); 151 | }, [filteredChords, shouldUpdateChords]); 152 | 153 | const clearTotalTimePracticed = () => { 154 | localStorage.removeItem('totalTimePracticed'); 155 | setTotalTimePracticed(0); 156 | handleDialogClose(); 157 | }; 158 | 159 | useEffect(() => { 160 | updateTargetChords(); 161 | }, [updateTargetChords]); 162 | 163 | useEffect(() => { 164 | const slicedChordLibrary = chordLibrary.slice(chordIndexValue[0]-1,chordIndexValue[1]-1); 165 | const newFilteredChords = slicedChordLibrary.filter(chord => { 166 | const chordLength = chord.chordInput.split('+').length; 167 | return chordLength >= sliderKeyValue[0] && chordLength <= sliderKeyValue[1]; 168 | }); 169 | let validChords = newFilteredChords.filter(isValidChord); 170 | setFilteredChords(validChords); 171 | 172 | if (validChords.length > 0) { 173 | setShouldUpdateChords(true); 174 | updateTargetChords(); 175 | } 176 | 177 | setCorrectInput(null); 178 | setChordStats({}); 179 | // eslint-disable-next-line react-hooks/exhaustive-deps 180 | }, [chordLibrary, sliderKeyValue, chordIndexValue]); 181 | 182 | useEffect(() => { 183 | resetState(); 184 | const storedTotalTime = localStorage.getItem('totalTimePracticed'); 185 | if (storedTotalTime) { 186 | setTotalTimePracticed(parseInt(storedTotalTime, 10)); 187 | } 188 | // eslint-disable-next-line react-hooks/exhaustive-deps 189 | }, []); 190 | 191 | useEffect(() => { 192 | let timerInterval; 193 | if (timerActive) { 194 | timerInterval = setInterval(() => { 195 | setTimerSeconds(prev => { 196 | if (prev <= 1) { 197 | clearInterval(timerInterval); 198 | setTimerActive(false); 199 | updateTotalTimePracticed(customTime * 60); 200 | if (inputRef.current) { 201 | inputRef.current.disabled = true; 202 | } 203 | return 0; 204 | } 205 | return prev - 1; 206 | }); 207 | }, 1000); 208 | } 209 | return () => clearInterval(timerInterval); 210 | // eslint-disable-next-line react-hooks/exhaustive-deps 211 | }, [timerActive]); 212 | 213 | const handleDialogOpen = () => { 214 | setOpenDialog(true); 215 | }; 216 | 217 | const handleDialogClose = () => { 218 | setOpenDialog(false); 219 | }; 220 | 221 | 222 | const sortedChords = Object.keys(chordStats) 223 | .map(chord => ({ 224 | chord, 225 | ...chordStats[chord] 226 | })) 227 | .sort((a, b) => (b.attempt - b.correct) - (a.attempt - a.correct)); 228 | 229 | const columns = [ 230 | { field: 'chord', headerName: 'Chord', width: 150 }, 231 | { field: 'attempt', headerName: 'Attempts', type: 'number', width: 150 }, 232 | { field: 'correct', headerName: 'Correct', type: 'number', width: 150 }, 233 | { 234 | field: 'percentage', 235 | headerName: 'Percentage', 236 | type: 'number', 237 | width: 150, 238 | valueFormatter: (params) => { 239 | if (params.value == null) { 240 | return ''; 241 | } 242 | return `${params.value.toLocaleString()} %`; 243 | } } 244 | ]; 245 | 246 | const rows = sortedChords.map(({ chord, attempt, correct }, index) => { 247 | const percentage = (attempt === 0) ? 0 : ((correct / attempt) * 100).toFixed(0); 248 | return { 249 | id: index, 250 | chord, 251 | attempt, 252 | correct, 253 | percentage 254 | }; 255 | }); 256 | 257 | const formatTotalTime = (totalSeconds) => { 258 | const secondsInADay = 86400; 259 | const secondsInAnHour = 3600; 260 | const secondsInAMinute = 60; 261 | 262 | const days = Math.floor(totalSeconds / secondsInADay); 263 | let remainingSeconds = totalSeconds % secondsInADay; 264 | 265 | const hours = Math.floor(remainingSeconds / secondsInAnHour); 266 | remainingSeconds = remainingSeconds % secondsInAnHour; 267 | 268 | const minutes = Math.floor(remainingSeconds / secondsInAMinute); 269 | 270 | // Build the formatted time string conditionally 271 | let formattedTime = ""; 272 | 273 | if (days > 0) { 274 | formattedTime += `${days} days, `; 275 | } 276 | 277 | if (totalSeconds >= secondsInAnHour) { 278 | formattedTime += `${hours} hours, `; 279 | } 280 | 281 | formattedTime += `${minutes} minutes`; 282 | 283 | return formattedTime; 284 | }; 285 | 286 | const formattedTime = formatTotalTime(totalTimePracticed); 287 | 288 | return ( 289 |
290 | 291 | 292 | 293 | Practice 294 | 295 | 296 | 297 | 298 | All time statistics: {formattedTime} 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | Do you really want to clear all time statistics? 307 | 308 | 309 | 312 | 318 | 319 | 320 | 321 | 322 | { 323 | chordLibrary.length > 0 ? ( 324 | <> 325 | 330 | 331 | 346 | 347 | 348 | 349 | Choose chords to practice: {chordIndexValue[0]} to {chordIndexValue[1]} 350 | 351 | 359 | 360 | 361 | 362 | Filter chords by number of keys: {filteredChords.length} chords have {sliderKeyValue[0]} - {sliderKeyValue[1]} keys 363 | 364 | 372 | 373 | 374 | 380 | { 381 | filteredChords.length === 0 ? ( 382 | 383 | No chords available for the selected range. 384 | 385 | ) : ( 386 | Array.isArray(targetChords) && targetChords.length > 0 ? ( 387 |
388 | 395 | {targetChords.map((chord, index) => ( 396 | 397 | 402 | {chord?.chordOutput ?? "N/A"} 403 | 404 | 405 | ))} 406 | 407 |
408 | 409 | ) : ( 410 | 411 | No target chords are available. 412 | 413 | ) 414 | ) 415 | } 416 | 424 | {correctInput && ( 425 | 426 | The correct input is: {correctInput} 427 | 428 | )} 429 |
430 | 431 | {timerActive && ( 432 | 436 | Time Remaining: {Math.floor(timerSeconds / 60)}:{timerSeconds % 60 < 10 ? '0' : ''}{timerSeconds % 60} 437 | 438 | )} 439 | 440 | {timerSeconds <= 0 && rows.length > 0 && ( 441 | <> 442 | 443 | {rows.length} chords practiced 444 |
445 | 450 |
451 | 452 | )} 453 | 454 | 455 | ) : ( 456 | 457 | Please load your chord library in settings to get started. 458 | 459 | ) 460 | } 461 |
462 | ); 463 | } 464 | 465 | export default Practice; 466 | -------------------------------------------------------------------------------- /src/pages/WordTools.js: -------------------------------------------------------------------------------- 1 | import { React, useState } from 'react'; 2 | import { TextField, Typography, Button, Divider } from '@mui/material'; 3 | import { Card, CardActions, CardContent, CardMedia } from '@mui/material'; 4 | import { TableContainer, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Grid } from '@mui/material'; 5 | import { calculateBigrams, createFrequencyTable, findMissingChords, getPhraseFrequency } from '../functions/wordAnalysis'; 6 | import LinearWithValueLabel from '../components/LinearWithValueLabel'; 7 | import bigrams from '../assets/bigrams.png'; 8 | import anagrams from '../assets/anagrams.png'; 9 | import wordsPng from '../assets/words.png'; 10 | import phrasesPng from '../assets/phrases.png'; 11 | import AnagramWorker from 'workerize-loader!../functions/anagramWorker'; // eslint-disable-line import/no-webpack-loader-syntax 12 | import createCsv from '../functions/createCsv'; 13 | 14 | function WordTools({ chordLibrary = [], setChordLibrary }) { 15 | const [textToAnalyze, setTextToAnalyze] = useState(''); 16 | const [currentAnalysis, setCurrentAnalysis] = useState(null); 17 | const [anagramPairs, setAnagramPairs] = useState([]); 18 | const [loadingAnagrams, setLoadingAnagrams] = useState(false); 19 | const [anagramProgress, setAnagramProgress] = useState(0); 20 | const [showResults, setShowResults] = useState(false); 21 | const [sortOrder, setSortOrder] = useState('asc'); 22 | const [bigramCounts, setBigramCounts] = useState({}); 23 | const [sortedBigramCounts, setSortedBigramCounts] = useState([]); 24 | const [frequencyMatrix, setFrequencyMatrix] = useState(Array(26).fill().map(() => Array(26).fill(0))); 25 | const [normalizedFrequencyMatrix, setNormalizedFrequencyMatrix] = useState(Array(26).fill().map(() => Array(26).fill(0))); 26 | const [sortedWords, setSortedWords] = useState([]); 27 | const [commonPhrases, setCommonPhrases] = useState({}); 28 | const [uniqueWordCount, setUniqueWordCount] = useState(0); 29 | const [chordWordCount, setChordWordCount] = useState(0); 30 | const anagramWorker = new AnagramWorker(); 31 | 32 | const handleChange = (event) => { 33 | setTextToAnalyze(event.target.value); 34 | }; 35 | 36 | const handleSortClick = () => { 37 | const sortedData = [...sortedBigramCounts].sort((a, b) => ( 38 | sortOrder === 'asc' ? a[1] - b[1] : b[1] - a[1] 39 | )); 40 | 41 | setSortedBigramCounts(sortedData); 42 | setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); 43 | }; 44 | 45 | const clickFindBigrams = () => { 46 | setCurrentAnalysis('bigram'); 47 | const [counts] = calculateBigrams(textToAnalyze); 48 | const [matrix, normalizedMatrix] = createFrequencyTable(counts); 49 | setFrequencyMatrix(matrix); 50 | setNormalizedFrequencyMatrix(normalizedMatrix); 51 | setBigramCounts(counts); 52 | const sortedCounts = Object.entries(counts).sort((a, b) => b[1] - a[1]); 53 | setSortedBigramCounts(sortedCounts); 54 | setShowResults(true); 55 | }; 56 | 57 | const clickPartialAnagrams = () => { 58 | setAnagramPairs([]); 59 | setShowResults(false); 60 | setLoadingAnagrams(true); 61 | setCurrentAnalysis('anagram'); 62 | anagramWorker.postMessage({ type: 'computeAnagrams', input: textToAnalyze }); 63 | }; 64 | 65 | anagramWorker.addEventListener('message', (event) => { 66 | if (event.data.type === 'progress') { 67 | setAnagramProgress(event.data.progress); 68 | } 69 | if (event.data.type === 'result') { 70 | setAnagramPairs(event.data.result); 71 | setShowResults(true); 72 | setLoadingAnagrams(false); 73 | } 74 | }); 75 | 76 | const clickWordStatistics = () => { 77 | setCurrentAnalysis('words'); 78 | (async () => { 79 | const [newSortedWords, newUniqueWordCount, newChordWordCount] = await findMissingChords(textToAnalyze, chordLibrary); 80 | setSortedWords(newSortedWords); 81 | setUniqueWordCount(newUniqueWordCount); 82 | setChordWordCount(newChordWordCount); 83 | })(); 84 | setShowResults(true); 85 | }; 86 | 87 | const clickPhraseStatistics = () => { 88 | setCurrentAnalysis('phrases'); 89 | const phrases = getPhraseFrequency(textToAnalyze, 3, 5, chordLibrary); 90 | setCommonPhrases(phrases); 91 | setShowResults(true); 92 | }; 93 | 94 | const downloadCsv = (words) => { 95 | const csvData = words.map(([word, frequency]) => { 96 | return { 97 | Word: word, 98 | Frequency: frequency, 99 | Score: word.length * frequency, 100 | }; 101 | }); 102 | 103 | createCsv(csvData, 'words.csv', true); 104 | } 105 | 106 | return ( 107 |
108 | 109 | Word Tools 110 | 111 | 121 | 122 | 123 | 124 | 125 | 130 | 131 | 132 | Bigram Statistics 133 | 134 | 135 | Bigrams are pairs of consecutive letters. While 136 | you can use 2 letter chords on your CharaChorder device, 137 | you might want to avoid common ones that you type so 138 | that you don't accidentally trigger chords while typing with 139 | character entry. 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 154 | 155 | 156 | Partial Anagrams 157 | 158 | 159 | Partial anagrams are words that share the same unique 160 | letters. These words should be considered closely when deciding chords, 161 | since you can't just use all of the letters of the word. 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 176 | 177 | 178 | Word Statistics 179 | 180 | 181 | Calculate the most frequent words in the text and identify any that 182 | you don't have in your chord library. 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 197 | 198 | 199 | Phrase Statistics 200 | 201 | 202 | Calculate the most frequent phrases in the text and identify any that 203 | you don't have in your chord library. 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | { 214 | showResults && ( 215 | <> 216 | Results 217 | 218 | 219 | ) 220 | } 221 | { 222 | loadingAnagrams && ( 223 | <> 224 | Analyzing the text for partial anagrams 225 | 226 | 227 | ) 228 | } 229 | { 230 | currentAnalysis === 'anagram' && ( 231 | anagramPairs && anagramPairs.length > 0 ? ( 232 | anagramPairs 233 | .map(pair => ({ 234 | pair, 235 | count: pair.reduce((sum, wordObj) => sum + wordObj.count, 0) 236 | })) 237 | .sort((a, b) => b.count - a.count) 238 | .map(({ pair }, index) => ( 239 | 240 | {pair.map(wordObj => wordObj.word + " (" + wordObj.count + ")").join(', ')} 241 | 242 | )) 243 | ) : ( 244 | showResults && No partial anagrams found in the text 245 | ) 246 | )} 247 | 248 | { 249 | currentAnalysis === 'bigram' && ( 250 | bigramCounts && ( 251 |
252 | Frequency Table 253 | {currentAnalysis === 'bigram' && ( 254 | 255 | 256 | 257 | 258 | {[...Array(26)].map((_, i) => ( 259 | {String.fromCharCode(65 + i)} 260 | ))} 261 | 262 | 263 | 264 | {frequencyMatrix.map((row, i) => ( 265 | 266 | {String.fromCharCode(65 + i)} 267 | {row.map((cell, j) => { 268 | const brightness = normalizedFrequencyMatrix[i][j]; 269 | const textColor = brightness < 128 ? 'black' : 'white'; 270 | return ( 271 | 272 | {cell.toFixed(1)} 273 | 274 | ); 275 | })} 276 | 277 | ))} 278 | 279 |
280 | )} 281 | Bigram List 282 |
283 | 284 | 285 | 286 | Bigram Pair 287 | 288 | 293 | Count 294 | 295 | 296 | 297 | 298 | 299 | {sortedBigramCounts.map(([bigram, count]) => ( 300 | 301 | {bigram} 302 | {count} 303 | 304 | ))} 305 | 306 |
307 |
308 | 309 |
310 | ) 311 | ) 312 | } 313 | { 314 | currentAnalysis === 'words' && ( 315 | <> 316 | {showResults && ( 317 | <> 318 | 319 | {chordLibrary.length > 0 320 | ? `You have chords for ${Math.round((chordWordCount / uniqueWordCount) * 100)}% (${chordWordCount} / ${uniqueWordCount}) of the unique words in the text (case insensitive).` 321 | : `There are ${uniqueWordCount} unique words in the text (case insensitive).`} 322 | 323 | 326 | 327 | 328 | 329 | 330 | Word 331 | Frequency 332 | Score (Length * Frequency) 333 | 334 | 335 | 336 | {sortedWords.map((word) => ( 337 | 338 | 339 | {word[0]} 340 | 341 | {word[1]} 342 | {word[0].length * Number(word[1])} 343 | 344 | ))} 345 | 346 |
347 |
348 | 349 | )} 350 | 351 | ) 352 | } 353 | { 354 | currentAnalysis === 'phrases' && ( 355 | <> 356 | {commonPhrases.length > 0 ? ( 357 | 358 | 359 | 360 | 361 | Phrase 362 | Frequency 363 | Score (Length * Frequency) 364 | 365 | 366 | 367 | {commonPhrases.map((phraseEntry) => ( 368 | 369 | 370 | {phraseEntry[0]} 371 | 372 | {phraseEntry[1]} 373 | {phraseEntry[0].split(' ').join('').length * Number(phraseEntry[1])} 374 | 375 | ))} 376 | 377 |
378 |
379 | ) : ( 380 | 381 | No phrases have been found. 382 | 383 | )} 384 | 385 | ) 386 | } 387 |
388 | ); 389 | } 390 | 391 | export default WordTools; -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { red } from '@mui/material/colors'; 2 | import { createTheme } from '@mui/material/styles'; 3 | 4 | // A custom theme for this app 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: '#272D2D', 9 | }, 10 | secondary: { 11 | main: '#A39BA8', 12 | }, 13 | error: { 14 | main: red.A400, 15 | }, 16 | }, 17 | }); 18 | 19 | export default theme; -------------------------------------------------------------------------------- /src/words/english-1000.json: -------------------------------------------------------------------------------- 1 | { 2 | "words": [ 3 | "the", 4 | "of", 5 | "to", 6 | "and", 7 | "a", 8 | "in", 9 | "is", 10 | "it", 11 | "you", 12 | "that", 13 | "he", 14 | "was", 15 | "for", 16 | "on", 17 | "are", 18 | "with", 19 | "as", 20 | "I", 21 | "his", 22 | "they", 23 | "be", 24 | "at", 25 | "one", 26 | "have", 27 | "this", 28 | "from", 29 | "or", 30 | "had", 31 | "by", 32 | "not", 33 | "word", 34 | "but", 35 | "what", 36 | "some", 37 | "we", 38 | "can", 39 | "out", 40 | "other", 41 | "were", 42 | "all", 43 | "there", 44 | "when", 45 | "up", 46 | "use", 47 | "your", 48 | "how", 49 | "said", 50 | "an", 51 | "each", 52 | "she", 53 | "which", 54 | "do", 55 | "their", 56 | "time", 57 | "if", 58 | "will", 59 | "way", 60 | "about", 61 | "many", 62 | "then", 63 | "them", 64 | "write", 65 | "would", 66 | "like", 67 | "so", 68 | "these", 69 | "her", 70 | "long", 71 | "make", 72 | "thing", 73 | "see", 74 | "him", 75 | "two", 76 | "has", 77 | "look", 78 | "more", 79 | "day", 80 | "could", 81 | "go", 82 | "come", 83 | "did", 84 | "number", 85 | "sound", 86 | "no", 87 | "most", 88 | "people", 89 | "my", 90 | "over", 91 | "know", 92 | "water", 93 | "than", 94 | "call", 95 | "first", 96 | "who", 97 | "may", 98 | "down", 99 | "side", 100 | "been", 101 | "now", 102 | "find", 103 | "any", 104 | "new", 105 | "work", 106 | "part", 107 | "take", 108 | "get", 109 | "place", 110 | "made", 111 | "live", 112 | "where", 113 | "after", 114 | "back", 115 | "little", 116 | "only", 117 | "round", 118 | "man", 119 | "year", 120 | "came", 121 | "show", 122 | "every", 123 | "good", 124 | "me", 125 | "give", 126 | "our", 127 | "under", 128 | "name", 129 | "very", 130 | "through", 131 | "just", 132 | "form", 133 | "sentence", 134 | "great", 135 | "think", 136 | "say", 137 | "help", 138 | "low", 139 | "line", 140 | "differ", 141 | "turn", 142 | "cause", 143 | "much", 144 | "mean", 145 | "before", 146 | "move", 147 | "right", 148 | "boy", 149 | "old", 150 | "too", 151 | "same", 152 | "tell", 153 | "does", 154 | "set", 155 | "three", 156 | "want", 157 | "air", 158 | "well", 159 | "also", 160 | "play", 161 | "small", 162 | "end", 163 | "put", 164 | "home", 165 | "read", 166 | "hand", 167 | "port", 168 | "large", 169 | "spell", 170 | "add", 171 | "even", 172 | "land", 173 | "here", 174 | "must", 175 | "big", 176 | "high", 177 | "such", 178 | "follow", 179 | "act", 180 | "why", 181 | "ask", 182 | "men", 183 | "change", 184 | "went", 185 | "light", 186 | "kind", 187 | "off", 188 | "need", 189 | "house", 190 | "picture", 191 | "try", 192 | "us", 193 | "again", 194 | "animal", 195 | "point", 196 | "mother", 197 | "world", 198 | "near", 199 | "build", 200 | "self", 201 | "earth", 202 | "father", 203 | "head", 204 | "stand", 205 | "own", 206 | "page", 207 | "should", 208 | "country", 209 | "found", 210 | "answer", 211 | "school", 212 | "grow", 213 | "study", 214 | "still", 215 | "learn", 216 | "plant", 217 | "cover", 218 | "food", 219 | "sun", 220 | "four", 221 | "between", 222 | "state", 223 | "keep", 224 | "eye", 225 | "never", 226 | "last", 227 | "let", 228 | "thought", 229 | "city", 230 | "tree", 231 | "cross", 232 | "farm", 233 | "hard", 234 | "start", 235 | "might", 236 | "story", 237 | "saw", 238 | "far", 239 | "sea", 240 | "draw", 241 | "left", 242 | "late", 243 | "run", 244 | "while", 245 | "press", 246 | "close", 247 | "night", 248 | "real", 249 | "life", 250 | "few", 251 | "north", 252 | "open", 253 | "seem", 254 | "together", 255 | "next", 256 | "white", 257 | "children", 258 | "begin", 259 | "got", 260 | "walk", 261 | "example", 262 | "ease", 263 | "paper", 264 | "group", 265 | "always", 266 | "music", 267 | "those", 268 | "both", 269 | "mark", 270 | "often", 271 | "letter", 272 | "until", 273 | "mile", 274 | "river", 275 | "car", 276 | "feet", 277 | "care", 278 | "second", 279 | "book", 280 | "carry", 281 | "took", 282 | "science", 283 | "eat", 284 | "room", 285 | "friend", 286 | "began", 287 | "idea", 288 | "fish", 289 | "mountain", 290 | "stop", 291 | "once", 292 | "base", 293 | "hear", 294 | "horse", 295 | "cut", 296 | "sure", 297 | "watch", 298 | "color", 299 | "face", 300 | "wood", 301 | "main", 302 | "enough", 303 | "plain", 304 | "girl", 305 | "usual", 306 | "young", 307 | "ready", 308 | "above", 309 | "ever", 310 | "red", 311 | "list", 312 | "though", 313 | "feel", 314 | "talk", 315 | "bird", 316 | "soon", 317 | "body", 318 | "dog", 319 | "family", 320 | "direct", 321 | "pose", 322 | "leave", 323 | "song", 324 | "measure", 325 | "door", 326 | "product", 327 | "black", 328 | "short", 329 | "numeral", 330 | "class", 331 | "wind", 332 | "question", 333 | "happen", 334 | "complete", 335 | "ship", 336 | "area", 337 | "half", 338 | "rock", 339 | "order", 340 | "fire", 341 | "south", 342 | "problem", 343 | "piece", 344 | "told", 345 | "knew", 346 | "pass", 347 | "since", 348 | "top", 349 | "whole", 350 | "king", 351 | "space", 352 | "heard", 353 | "best", 354 | "hour", 355 | "better", 356 | "true", 357 | "during", 358 | "hundred", 359 | "five", 360 | "remember", 361 | "step", 362 | "early", 363 | "hold", 364 | "west", 365 | "ground", 366 | "interest", 367 | "reach", 368 | "fast", 369 | "verb", 370 | "sing", 371 | "listen", 372 | "six", 373 | "table", 374 | "travel", 375 | "less", 376 | "morning", 377 | "ten", 378 | "simple", 379 | "several", 380 | "vowel", 381 | "toward", 382 | "war", 383 | "lay", 384 | "against", 385 | "pattern", 386 | "slow", 387 | "center", 388 | "love", 389 | "person", 390 | "money", 391 | "serve", 392 | "appear", 393 | "road", 394 | "map", 395 | "rain", 396 | "rule", 397 | "govern", 398 | "pull", 399 | "cold", 400 | "notice", 401 | "voice", 402 | "unit", 403 | "power", 404 | "town", 405 | "fine", 406 | "certain", 407 | "fly", 408 | "fall", 409 | "lead", 410 | "cry", 411 | "dark", 412 | "machine", 413 | "note", 414 | "wait", 415 | "plan", 416 | "figure", 417 | "star", 418 | "box", 419 | "noun", 420 | "field", 421 | "rest", 422 | "correct", 423 | "able", 424 | "pound", 425 | "done", 426 | "beauty", 427 | "drive", 428 | "stood", 429 | "contain", 430 | "front", 431 | "teach", 432 | "week", 433 | "final", 434 | "gave", 435 | "green", 436 | "oh", 437 | "quick", 438 | "develop", 439 | "ocean", 440 | "warm", 441 | "free", 442 | "minute", 443 | "strong", 444 | "special", 445 | "mind", 446 | "behind", 447 | "clear", 448 | "tail", 449 | "produce", 450 | "fact", 451 | "street", 452 | "inch", 453 | "multiply", 454 | "nothing", 455 | "course", 456 | "stay", 457 | "wheel", 458 | "full", 459 | "force", 460 | "blue", 461 | "object", 462 | "decide", 463 | "surface", 464 | "deep", 465 | "moon", 466 | "island", 467 | "foot", 468 | "system", 469 | "busy", 470 | "test", 471 | "record", 472 | "boat", 473 | "common", 474 | "gold", 475 | "possible", 476 | "plane", 477 | "stead", 478 | "dry", 479 | "wonder", 480 | "laugh", 481 | "thousand", 482 | "ago", 483 | "ran", 484 | "check", 485 | "game", 486 | "shape", 487 | "equate", 488 | "hot", 489 | "miss", 490 | "brought", 491 | "heat", 492 | "snow", 493 | "tire", 494 | "bring", 495 | "yes", 496 | "distant", 497 | "fill", 498 | "east", 499 | "paint", 500 | "language", 501 | "among", 502 | "grand", 503 | "ball", 504 | "yet", 505 | "wave", 506 | "drop", 507 | "heart", 508 | "am", 509 | "present", 510 | "heavy", 511 | "dance", 512 | "engine", 513 | "position", 514 | "arm", 515 | "wide", 516 | "sail", 517 | "material", 518 | "size", 519 | "vary", 520 | "settle", 521 | "speak", 522 | "weight", 523 | "general", 524 | "ice", 525 | "matter", 526 | "circle", 527 | "pair", 528 | "include", 529 | "divide", 530 | "syllable", 531 | "felt", 532 | "perhaps", 533 | "pick", 534 | "sudden", 535 | "count", 536 | "square", 537 | "reason", 538 | "length", 539 | "represent", 540 | "art", 541 | "subject", 542 | "region", 543 | "energy", 544 | "hunt", 545 | "probable", 546 | "bed", 547 | "brother", 548 | "egg", 549 | "ride", 550 | "cell", 551 | "believe", 552 | "fraction", 553 | "forest", 554 | "sit", 555 | "race", 556 | "window", 557 | "store", 558 | "summer", 559 | "train", 560 | "sleep", 561 | "prove", 562 | "lone", 563 | "leg", 564 | "exercise", 565 | "wall", 566 | "catch", 567 | "mount", 568 | "wish", 569 | "sky", 570 | "board", 571 | "joy", 572 | "winter", 573 | "sat", 574 | "written", 575 | "wild", 576 | "instrument", 577 | "kept", 578 | "glass", 579 | "grass", 580 | "cow", 581 | "job", 582 | "edge", 583 | "sign", 584 | "visit", 585 | "past", 586 | "soft", 587 | "fun", 588 | "bright", 589 | "gas", 590 | "weather", 591 | "month", 592 | "million", 593 | "bear", 594 | "finish", 595 | "happy", 596 | "hope", 597 | "flower", 598 | "clothes", 599 | "strange", 600 | "gone", 601 | "jump", 602 | "baby", 603 | "eight", 604 | "village", 605 | "meet", 606 | "root", 607 | "buy", 608 | "raise", 609 | "solve", 610 | "metal", 611 | "whether", 612 | "push", 613 | "seven", 614 | "paragraph", 615 | "third", 616 | "shall", 617 | "held", 618 | "hair", 619 | "describe", 620 | "cook", 621 | "floor", 622 | "either", 623 | "result", 624 | "burn", 625 | "hill", 626 | "safe", 627 | "cat", 628 | "century", 629 | "consider", 630 | "type", 631 | "law", 632 | "bit", 633 | "coast", 634 | "copy", 635 | "phrase", 636 | "silent", 637 | "tall", 638 | "sand", 639 | "soil", 640 | "roll", 641 | "temperature", 642 | "finger", 643 | "industry", 644 | "value", 645 | "fight", 646 | "lie", 647 | "beat", 648 | "excite", 649 | "natural", 650 | "view", 651 | "sense", 652 | "ear", 653 | "else", 654 | "quite", 655 | "broke", 656 | "case", 657 | "middle", 658 | "kill", 659 | "son", 660 | "lake", 661 | "moment", 662 | "scale", 663 | "loud", 664 | "spring", 665 | "observe", 666 | "child", 667 | "straight", 668 | "consonant", 669 | "nation", 670 | "dictionary", 671 | "milk", 672 | "speed", 673 | "method", 674 | "organ", 675 | "pay", 676 | "age", 677 | "section", 678 | "dress", 679 | "cloud", 680 | "surprise", 681 | "quiet", 682 | "stone", 683 | "tiny", 684 | "climb", 685 | "cool", 686 | "design", 687 | "poor", 688 | "lot", 689 | "experiment", 690 | "bottom", 691 | "key", 692 | "iron", 693 | "single", 694 | "stick", 695 | "flat", 696 | "twenty", 697 | "skin", 698 | "smile", 699 | "crease", 700 | "hole", 701 | "trade", 702 | "melody", 703 | "trip", 704 | "office", 705 | "receive", 706 | "row", 707 | "mouth", 708 | "exact", 709 | "symbol", 710 | "die", 711 | "least", 712 | "trouble", 713 | "shout", 714 | "except", 715 | "wrote", 716 | "seed", 717 | "tone", 718 | "join", 719 | "suggest", 720 | "clean", 721 | "break", 722 | "lady", 723 | "yard", 724 | "rise", 725 | "bad", 726 | "blow", 727 | "oil", 728 | "blood", 729 | "touch", 730 | "grew", 731 | "cent", 732 | "mix", 733 | "team", 734 | "wire", 735 | "cost", 736 | "lost", 737 | "brown", 738 | "wear", 739 | "garden", 740 | "equal", 741 | "sent", 742 | "choose", 743 | "fell", 744 | "fit", 745 | "flow", 746 | "fair", 747 | "bank", 748 | "collect", 749 | "save", 750 | "control", 751 | "decimal", 752 | "gentle", 753 | "woman", 754 | "captain", 755 | "practice", 756 | "separate", 757 | "difficult", 758 | "doctor", 759 | "please", 760 | "protect", 761 | "noon", 762 | "whose", 763 | "locate", 764 | "ring", 765 | "character", 766 | "insect", 767 | "caught", 768 | "period", 769 | "indicate", 770 | "radio", 771 | "spoke", 772 | "atom", 773 | "human", 774 | "history", 775 | "effect", 776 | "electric", 777 | "expect", 778 | "crop", 779 | "modern", 780 | "element", 781 | "hit", 782 | "student", 783 | "corner", 784 | "party", 785 | "supply", 786 | "bone", 787 | "rail", 788 | "imagine", 789 | "provide", 790 | "agree", 791 | "thus", 792 | "capital", 793 | "chair", 794 | "danger", 795 | "fruit", 796 | "rich", 797 | "thick", 798 | "soldier", 799 | "process", 800 | "operate", 801 | "guess", 802 | "necessary", 803 | "sharp", 804 | "wing", 805 | "create", 806 | "neighbor", 807 | "wash", 808 | "bat", 809 | "rather", 810 | "crowd", 811 | "corn", 812 | "compare", 813 | "poem", 814 | "string", 815 | "bell", 816 | "depend", 817 | "meat", 818 | "rub", 819 | "tube", 820 | "famous", 821 | "dollar", 822 | "stream", 823 | "fear", 824 | "sight", 825 | "thin", 826 | "triangle", 827 | "planet", 828 | "hurry", 829 | "chief", 830 | "colony", 831 | "clock", 832 | "mine", 833 | "tie", 834 | "enter", 835 | "major", 836 | "fresh", 837 | "search", 838 | "send", 839 | "yellow", 840 | "gun", 841 | "allow", 842 | "print", 843 | "dead", 844 | "spot", 845 | "desert", 846 | "suit", 847 | "current", 848 | "lift", 849 | "rose", 850 | "continue", 851 | "block", 852 | "chart", 853 | "hat", 854 | "sell", 855 | "success", 856 | "company", 857 | "subtract", 858 | "event", 859 | "particular", 860 | "deal", 861 | "swim", 862 | "term", 863 | "opposite", 864 | "wife", 865 | "shoe", 866 | "shoulder", 867 | "spread", 868 | "arrange", 869 | "camp", 870 | "invent", 871 | "cotton", 872 | "born", 873 | "determine", 874 | "quart", 875 | "nine", 876 | "truck", 877 | "noise", 878 | "level", 879 | "chance", 880 | "gather", 881 | "shop", 882 | "stretch", 883 | "throw", 884 | "shine", 885 | "property", 886 | "column", 887 | "molecule", 888 | "select", 889 | "wrong", 890 | "gray", 891 | "repeat", 892 | "require", 893 | "broad", 894 | "prepare", 895 | "salt", 896 | "nose", 897 | "plural", 898 | "anger", 899 | "claim", 900 | "continent", 901 | "oxygen", 902 | "sugar", 903 | "death", 904 | "pretty", 905 | "skill", 906 | "women", 907 | "season", 908 | "solution", 909 | "magnet", 910 | "silver", 911 | "thank", 912 | "branch", 913 | "match", 914 | "suffix", 915 | "especially", 916 | "fig", 917 | "afraid", 918 | "huge", 919 | "sister", 920 | "steel", 921 | "discuss", 922 | "forward", 923 | "similar", 924 | "guide", 925 | "experience", 926 | "score", 927 | "apple", 928 | "bought", 929 | "led", 930 | "pitch", 931 | "coat", 932 | "mass", 933 | "card", 934 | "band", 935 | "rope", 936 | "slip", 937 | "win", 938 | "dream", 939 | "evening", 940 | "condition", 941 | "feed", 942 | "tool", 943 | "total", 944 | "basic", 945 | "smell", 946 | "valley", 947 | "nor", 948 | "double", 949 | "seat", 950 | "arrive", 951 | "master", 952 | "track", 953 | "parent", 954 | "shore", 955 | "division", 956 | "sheet", 957 | "substance", 958 | "favor", 959 | "connect", 960 | "post", 961 | "spend", 962 | "chord", 963 | "fat", 964 | "glad", 965 | "original", 966 | "share", 967 | "station", 968 | "dad", 969 | "bread", 970 | "charge", 971 | "proper", 972 | "bar", 973 | "offer", 974 | "segment", 975 | "duck", 976 | "instant", 977 | "market", 978 | "degree", 979 | "populate", 980 | "chick", 981 | "dear", 982 | "enemy", 983 | "reply", 984 | "drink", 985 | "occur", 986 | "support", 987 | "speech", 988 | "nature", 989 | "range", 990 | "steam", 991 | "motion", 992 | "path", 993 | "liquid", 994 | "log", 995 | "meant", 996 | "quotient", 997 | "teeth", 998 | "shell", 999 | "neck" 1000 | ] 1001 | } -------------------------------------------------------------------------------- /src/words/english-500.json: -------------------------------------------------------------------------------- 1 | { 2 | "words": [ 3 | "the", 4 | "of", 5 | "to", 6 | "and", 7 | "a", 8 | "in", 9 | "is", 10 | "it", 11 | "you", 12 | "that", 13 | "he", 14 | "was", 15 | "for", 16 | "on", 17 | "are", 18 | "with", 19 | "as", 20 | "I", 21 | "his", 22 | "they", 23 | "be", 24 | "at", 25 | "one", 26 | "have", 27 | "this", 28 | "from", 29 | "or", 30 | "had", 31 | "by", 32 | "not", 33 | "word", 34 | "but", 35 | "what", 36 | "some", 37 | "we", 38 | "can", 39 | "out", 40 | "other", 41 | "were", 42 | "all", 43 | "there", 44 | "when", 45 | "up", 46 | "use", 47 | "your", 48 | "how", 49 | "said", 50 | "an", 51 | "each", 52 | "she", 53 | "which", 54 | "do", 55 | "their", 56 | "time", 57 | "if", 58 | "will", 59 | "way", 60 | "about", 61 | "many", 62 | "then", 63 | "them", 64 | "write", 65 | "would", 66 | "like", 67 | "so", 68 | "these", 69 | "her", 70 | "long", 71 | "make", 72 | "thing", 73 | "see", 74 | "him", 75 | "two", 76 | "has", 77 | "look", 78 | "more", 79 | "day", 80 | "could", 81 | "go", 82 | "come", 83 | "did", 84 | "number", 85 | "sound", 86 | "no", 87 | "most", 88 | "people", 89 | "my", 90 | "over", 91 | "know", 92 | "water", 93 | "than", 94 | "call", 95 | "first", 96 | "who", 97 | "may", 98 | "down", 99 | "side", 100 | "been", 101 | "now", 102 | "find", 103 | "any", 104 | "new", 105 | "work", 106 | "part", 107 | "take", 108 | "get", 109 | "place", 110 | "made", 111 | "live", 112 | "where", 113 | "after", 114 | "back", 115 | "little", 116 | "only", 117 | "round", 118 | "man", 119 | "year", 120 | "came", 121 | "show", 122 | "every", 123 | "good", 124 | "me", 125 | "give", 126 | "our", 127 | "under", 128 | "name", 129 | "very", 130 | "through", 131 | "just", 132 | "form", 133 | "sentence", 134 | "great", 135 | "think", 136 | "say", 137 | "help", 138 | "low", 139 | "line", 140 | "differ", 141 | "turn", 142 | "cause", 143 | "much", 144 | "mean", 145 | "before", 146 | "move", 147 | "right", 148 | "boy", 149 | "old", 150 | "too", 151 | "same", 152 | "tell", 153 | "does", 154 | "set", 155 | "three", 156 | "want", 157 | "air", 158 | "well", 159 | "also", 160 | "play", 161 | "small", 162 | "end", 163 | "put", 164 | "home", 165 | "read", 166 | "hand", 167 | "port", 168 | "large", 169 | "spell", 170 | "add", 171 | "even", 172 | "land", 173 | "here", 174 | "must", 175 | "big", 176 | "high", 177 | "such", 178 | "follow", 179 | "act", 180 | "why", 181 | "ask", 182 | "men", 183 | "change", 184 | "went", 185 | "light", 186 | "kind", 187 | "off", 188 | "need", 189 | "house", 190 | "picture", 191 | "try", 192 | "us", 193 | "again", 194 | "animal", 195 | "point", 196 | "mother", 197 | "world", 198 | "near", 199 | "build", 200 | "self", 201 | "earth", 202 | "father", 203 | "head", 204 | "stand", 205 | "own", 206 | "page", 207 | "should", 208 | "country", 209 | "found", 210 | "answer", 211 | "school", 212 | "grow", 213 | "study", 214 | "still", 215 | "learn", 216 | "plant", 217 | "cover", 218 | "food", 219 | "sun", 220 | "four", 221 | "between", 222 | "state", 223 | "keep", 224 | "eye", 225 | "never", 226 | "last", 227 | "let", 228 | "thought", 229 | "city", 230 | "tree", 231 | "cross", 232 | "farm", 233 | "hard", 234 | "start", 235 | "might", 236 | "story", 237 | "saw", 238 | "far", 239 | "sea", 240 | "draw", 241 | "left", 242 | "late", 243 | "run", 244 | "while", 245 | "press", 246 | "close", 247 | "night", 248 | "real", 249 | "life", 250 | "few", 251 | "north", 252 | "open", 253 | "seem", 254 | "together", 255 | "next", 256 | "white", 257 | "children", 258 | "begin", 259 | "got", 260 | "walk", 261 | "example", 262 | "ease", 263 | "paper", 264 | "group", 265 | "always", 266 | "music", 267 | "those", 268 | "both", 269 | "mark", 270 | "often", 271 | "letter", 272 | "until", 273 | "mile", 274 | "river", 275 | "car", 276 | "feet", 277 | "care", 278 | "second", 279 | "book", 280 | "carry", 281 | "took", 282 | "science", 283 | "eat", 284 | "room", 285 | "friend", 286 | "began", 287 | "idea", 288 | "fish", 289 | "mountain", 290 | "stop", 291 | "once", 292 | "base", 293 | "hear", 294 | "horse", 295 | "cut", 296 | "sure", 297 | "watch", 298 | "color", 299 | "face", 300 | "wood", 301 | "main", 302 | "enough", 303 | "plain", 304 | "girl", 305 | "usual", 306 | "young", 307 | "ready", 308 | "above", 309 | "ever", 310 | "red", 311 | "list", 312 | "though", 313 | "feel", 314 | "talk", 315 | "bird", 316 | "soon", 317 | "body", 318 | "dog", 319 | "family", 320 | "direct", 321 | "pose", 322 | "leave", 323 | "song", 324 | "measure", 325 | "door", 326 | "product", 327 | "black", 328 | "short", 329 | "numeral", 330 | "class", 331 | "wind", 332 | "question", 333 | "happen", 334 | "complete", 335 | "ship", 336 | "area", 337 | "half", 338 | "rock", 339 | "order", 340 | "fire", 341 | "south", 342 | "problem", 343 | "piece", 344 | "told", 345 | "knew", 346 | "pass", 347 | "since", 348 | "top", 349 | "whole", 350 | "king", 351 | "space", 352 | "heard", 353 | "best", 354 | "hour", 355 | "better", 356 | "true", 357 | "during", 358 | "hundred", 359 | "five", 360 | "remember", 361 | "step", 362 | "early", 363 | "hold", 364 | "west", 365 | "ground", 366 | "interest", 367 | "reach", 368 | "fast", 369 | "verb", 370 | "sing", 371 | "listen", 372 | "six", 373 | "table", 374 | "travel", 375 | "less", 376 | "morning", 377 | "ten", 378 | "simple", 379 | "several", 380 | "vowel", 381 | "toward", 382 | "war", 383 | "lay", 384 | "against", 385 | "pattern", 386 | "slow", 387 | "center", 388 | "love", 389 | "person", 390 | "money", 391 | "serve", 392 | "appear", 393 | "road", 394 | "map", 395 | "rain", 396 | "rule", 397 | "govern", 398 | "pull", 399 | "cold", 400 | "notice", 401 | "voice", 402 | "unit", 403 | "power", 404 | "town", 405 | "fine", 406 | "certain", 407 | "fly", 408 | "fall", 409 | "lead", 410 | "cry", 411 | "dark", 412 | "machine", 413 | "note", 414 | "wait", 415 | "plan", 416 | "figure", 417 | "star", 418 | "box", 419 | "noun", 420 | "field", 421 | "rest", 422 | "correct", 423 | "able", 424 | "pound", 425 | "done", 426 | "beauty", 427 | "drive", 428 | "stood", 429 | "contain", 430 | "front", 431 | "teach", 432 | "week", 433 | "final", 434 | "gave", 435 | "green", 436 | "oh", 437 | "quick", 438 | "develop", 439 | "ocean", 440 | "warm", 441 | "free", 442 | "minute", 443 | "strong", 444 | "special", 445 | "mind", 446 | "behind", 447 | "clear", 448 | "tail", 449 | "produce", 450 | "fact", 451 | "street", 452 | "inch", 453 | "multiply", 454 | "nothing", 455 | "course", 456 | "stay", 457 | "wheel", 458 | "full", 459 | "force", 460 | "blue", 461 | "object", 462 | "decide", 463 | "surface", 464 | "deep", 465 | "moon", 466 | "island", 467 | "foot", 468 | "system", 469 | "busy", 470 | "test", 471 | "record", 472 | "boat", 473 | "common", 474 | "gold", 475 | "possible", 476 | "plane", 477 | "stead", 478 | "dry", 479 | "wonder", 480 | "laugh", 481 | "thousand", 482 | "ago", 483 | "ran", 484 | "check", 485 | "game", 486 | "shape", 487 | "equate", 488 | "hot", 489 | "miss", 490 | "brought", 491 | "heat", 492 | "snow", 493 | "tire", 494 | "bring", 495 | "yes", 496 | "distant", 497 | "fill", 498 | "east", 499 | "paint", 500 | "language", 501 | "among" 502 | ] 503 | } --------------------------------------------------------------------------------