-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathindex.html
215 lines (184 loc) · 8.58 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<!DOCTYPE HTML>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/pace-js@latest/pace.min.js"></script>
<!--link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pace-js@latest/pace-theme-default.min.css"-->
<link rel="stylesheet" href="loading-bar.css">
<link rel="stylesheet" href="style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orelega+One&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
</head>
<h2><span class="title">Tiny Predictive Text</span>
<br><small>The Predictive Text Model That Runs in The Browser and Fits on a Floppy Disk</small></h2>
<p>Press <strong>Tab</strong> to autocomplete with suggestion or just <strong>tap</strong> on it.<br>
Press <strong>Shift</strong> to change the suggestion or <strong>tap</strong> on the quality score.</p>
<p id="loading" class="loading">Loading model...</p>
<main class="box">
Context string: <code><span id="cs-layer1">...</span> → <span id="cs-layer2">...</span> → <span id="cs-layer3">...</span></code>
<br>Predictive quality score: <div class="quality-score" id="quality-score">...</div>
<br>
<div id="entry-holder" class="entry-holder">
<div id="entry" class="entry" contenteditable="true""> </div><span id="suggestion-ghost" class="ghost"></span>
</div>
</main>
<h3>Quality threshold</h3>
<label>
<input type="radio" name="quality"> <span class="quality-preview">...</span> None (More results, less accurate)
</label>
<label>
<input type="radio" name="quality"> <span class="quality-preview">...</span> Low
</label>
<label>
<input type="radio" name="quality"> <span class="quality-preview">...</span> Medium (Enough results, good accuracy)
</label>
<label>
<input type="radio" name="quality"> <span class="quality-preview">...</span> High
</label>
<label>
<input type="radio" name="quality"> <span class="quality-preview">...</span> Very High (Fewer results, most accurate)
</label>
<p>Tiny Predictive Text uses the ArT DeCo (Anchor Tree with Decaying Context) technique to generate sentence completions.</p>
<p>For optimum efficiency and file size, Tiny is built in Rust and compiled to Wasm for plug-and-play implementation in frontend JavaScript using tokenized dictionaries compiled to Message Pack...<a href="https://adamgrant.info/tiny-predictive-text">(Keep reading)</a></p>
<p><a href="https://github.com/adamjgrant/Tiny-Predictive-Text">Use in your project</a></p>
<script type="module" src="dist/tinypredict.js"></script>
<script>
const entry = document.getElementById('entry');
const suggestion_element = document.getElementById('suggestion');
const other_suggestions_element = document.getElementById('other-suggestions');
const entry_holder = document.getElementById('entry-holder');
const ghost = document.getElementById('suggestion-ghost');
let predictions_on_deck = [];
let last_suggestions = {};
const quality_thresholds = [0, 29, 43, 60, 80];
let quality_threshold = quality_thresholds[2];
const set_suggestion_element = (force_remove_space=false, clear=false) => {
if (clear) {
ghost.innerHTML = '';
predictions_on_deck = [];
return;
}
if (predictions_on_deck.length === 0 || predictions_on_deck[0] === undefined || predictions_on_deck[0].completion === undefined || predictions_on_deck[0].completion === "") {
predictions_on_deck = [];
ghost.innerHTML = '';
return;
}
ghost.innerHTML = predictions_on_deck[0].completion;
if (!entry.innerText.split("").reverse()[0].match(/\s/) && !force_remove_space) {
ghost.innerHTML = ` ${ghost.innerHTML.trim()}`;
}
}
const completeSuggestion = () => {
if (predictions_on_deck.length === 0) {
entry.innerText = entry.innerText.trim() + " ";
return moveCursorToEnd(entry);
}
entry.innerText = entry.innerText.trim() + " " + predictions_on_deck[0].completion.trim() + " ";
predictions_on_deck = [];
set_suggestion_element();
moveCursorToEnd(entry);
}
// Copy <templates> by ID.
const copy_from_template = (template_id) => document.getElementById(template_id).content.cloneNode(true).firstElementChild;
const cs_layer1 = document.getElementById('cs-layer1');
const cs_layer2 = document.getElementById('cs-layer2');
const cs_layer3 = document.getElementById('cs-layer3');
const quality_score = document.getElementById('quality-score');
const quality_score_names = ["terrible", "bad", "good", "better", "best"];
const quality_score_colors_gte = quality_thresholds.map((threshold, index) => {
return [threshold, quality_score_names[index]];
}).reduce((acc, [threshold, name]) => {
acc[threshold] = name;
return acc;
}, {});
const set_quality_score_color = (score) => {
Object.values(quality_score_colors_gte).forEach(classname => quality_score.classList.remove(classname));
const key = Object.keys(quality_score_colors_gte).map(n => parseInt(n)).reverse().find(key => score >= parseInt(key));
const classname = quality_score_colors_gte[key];
quality_score.classList.add(classname);
}
const set_debugger = (suggestions) => {
cs_layer1.innerHTML = suggestions.second_level_context;
cs_layer2.innerHTML = suggestions.first_level_context;
cs_layer3.innerHTML = suggestions.anchor
let quality_score_value = 0;
if (predictions_on_deck.length && predictions_on_deck[0] && predictions_on_deck[0].quality) {
quality_score_value = predictions_on_deck[0].quality;
}
else if (suggestions.prediction && suggestions.prediction.length) {
quality_score_value = suggestions.prediction[0].quality;
}
quality_score.innerHTML = quality_score_value;
set_quality_score_color(quality_score_value);
}
function moveCursorToEnd(contentEditableElement) {
// Ensure the element is focusable
contentEditableElement.focus();
// Create a range object
var range = document.createRange();
var selection = window.getSelection();
// Set the range to the end of the content
range.selectNodeContents(contentEditableElement);
range.collapse(false); // false collapses the range to its end
// Remove any existing selections
selection.removeAllRanges();
// Add the new range
selection.addRange(range);
}
window.addEventListener('tinypredict-ready', () => {
document.body.classList.add('loaded');
const get_predictions = (event) => {
}
const entry = document.getElementById('entry');
entry_holder.addEventListener("click", (e) => {
entry.focus();
moveCursorToEnd(entry);
});
entry.addEventListener("keydown", function(event) {
if (event.key === "Tab") {
event.preventDefault();
completeSuggestion();
} else if (event.key === " ") {
set_suggestion_element(true);
}
});
ghost.addEventListener("click", (e) => {
completeSuggestion();
});
quality_score.addEventListener("click", (e) => {
predictions_on_deck.push(predictions_on_deck.shift());
set_suggestion_element();
});
entry.addEventListener("keyup", function(event) {
if (encodeURI(entry.innerText) === "%0A") { entry.innerHTML = ""; }
if (event.key === "Shift") {
event.preventDefault();
predictions_on_deck.push(predictions_on_deck.shift());
set_suggestion_element();
set_debugger(last_suggestions);
}
else {
const punctuation_regex = /[.\?&\*;:{}=\-_`~()]+/g;
const input = event.target.innerText.split(punctuation_regex).pop().trim();
window.getPredictiveText(input).then(suggestions => {
last_suggestions = suggestions;
set_debugger(suggestions);
predictions_on_deck = suggestions.prediction.filter(_prediction => _prediction.quality >= quality_threshold);
set_suggestion_element()
});
}
});
const radios = document.querySelectorAll('input[name="quality"]');
radios.forEach((radio, index) => {
const threshold = quality_thresholds[index];
radio.setAttribute('value', threshold);
if (threshold === quality_threshold) { radio.checked = true; }
radio.parentElement.querySelector(".quality-preview").innerHTML = threshold;
radio.addEventListener('change', function() {
quality_threshold = this.value;
console.log('Quality Threshold:', this.value);
});
});
});
</script>