Skip to content

Commit fcf4c22

Browse files
committed
Quick search + Scatt AI
1 parent 7e68379 commit fcf4c22

File tree

5 files changed

+491
-0
lines changed

5 files changed

+491
-0
lines changed

features/features.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"version": 2,
4+
"id": "quick-search",
5+
"versionAdded": "v3.7.0"
6+
},
27
{
38
"version": 2,
49
"id": "favicon-messages",

features/quick-search/data.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"title": "Quick Search",
3+
"description": "Easily jump between pages on the Scratch website with just the click of Control + K.",
4+
"credits": [
5+
{
6+
"username": "rgantzos",
7+
"url": "https://scratch.mit.edu/users/rgantzos/"
8+
}
9+
],
10+
"dynamic": true,
11+
"styles": [{ "file": "style.css", "runOn": "/*" }],
12+
"scripts": [{ "file": "script.js", "runOn": "/*" }],
13+
"tags": ["New"],
14+
"type": ["Website"],
15+
"components": [
16+
{
17+
"type": "info",
18+
"content": "For Mac users, use Command + K to open quick search.",
19+
"if": {
20+
"type": "all",
21+
"conditions": [
22+
{
23+
"type": "os",
24+
"value": "Macintosh"
25+
}
26+
]
27+
}
28+
}
29+
],
30+
"resources": [{ "name": "scatt", "path": "/scatt.png" }]
31+
}

features/quick-search/scatt.png

506 KB
Loading

features/quick-search/script.js

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
export default async function ({ feature, console }) {
2+
document.querySelector(".ste-quick-search")?.remove();
3+
4+
let search = createQuickSearch();
5+
6+
function aiResponse(text, loading) {
7+
document.querySelector(".ai-response")?.remove();
8+
let div = document.createElement("div");
9+
div.className = "ai-response";
10+
div.innerHTML = "<div></div><div></div>";
11+
div.lastChild.textContent = text;
12+
13+
let img = document.createElement("img");
14+
img.src = feature.self.getResource("scatt");
15+
div.firstChild.appendChild(img);
16+
17+
if (search.div.dataset.mode === "ai") {
18+
search.div.firstChild.querySelector("div").appendChild(div);
19+
}
20+
21+
return div;
22+
}
23+
24+
document.addEventListener("keydown", async function (e) {
25+
if (e.which === 75 && e.metaKey) {
26+
e.preventDefault();
27+
search.div.style.display = null;
28+
search.input.focus();
29+
search.input.value = "";
30+
search.div.dataset.mode = "normal";
31+
search.setResults(getDefaultResults());
32+
getAdditionalResults(search.input);
33+
}
34+
if (
35+
e.which === 13 &&
36+
document.activeElement === search.input &&
37+
!search.div.style.display
38+
) {
39+
if (search.div.dataset.mode === "ai") {
40+
let query = search.input.value;
41+
aiResponse("AI is currently responding to your request...", true);
42+
let username = (await feature.auth.fetch())?.user?.username || "";
43+
let data = await (
44+
await fetch("https://data.scratchtools.app/ai-query/", {
45+
method: "POST",
46+
headers: {
47+
Accept: "application/json",
48+
"Content-Type": "application/json",
49+
},
50+
body: JSON.stringify({
51+
username,
52+
search: search.input.value.replace("Scatt", ""),
53+
}),
54+
})
55+
).json();
56+
if (query === search.input.value) {
57+
search.input.value = "Scatt ";
58+
if (typeof data.response === "string") {
59+
aiResponse(data.response, false);
60+
} else if (data.success) {
61+
if (data.response.length) {
62+
let div = aiResponse(
63+
"Here are some projects I've found for you:",
64+
false
65+
);
66+
67+
div.lastChild.appendChild(document.createElement("div"));
68+
69+
for (var i in data.response) {
70+
let a = document.createElement("a");
71+
a.textContent = data.response[i].name;
72+
a.href = data.response[i].url;
73+
div.lastChild.lastChild.appendChild(a);
74+
}
75+
} else {
76+
aiResponse(
77+
"Sorry, I couldn't find any projects like that.",
78+
false
79+
);
80+
}
81+
} else {
82+
aiResponse("Sorry, something went wrong with your request.", false);
83+
}
84+
}
85+
} else {
86+
if (search.div.querySelector("a")) {
87+
search.div.querySelector("a")?.click();
88+
}
89+
}
90+
}
91+
if (e.which === 27) {
92+
search.div.style.display = "none";
93+
}
94+
});
95+
96+
search.input.addEventListener("input", inputResults);
97+
98+
function inputResults() {
99+
document.querySelector(".qs-ai-instructions")?.remove();
100+
if (search.input.value.startsWith("Scatt")) {
101+
document.querySelector(".qs-escape-instructions").style.display = "none";
102+
let span = document.createElement("span");
103+
span.className = "qs-ai-instructions";
104+
span.innerHTML = `<span class='qs-key'>AI</span> mode entered.`;
105+
span.firstChild.style.borderColor = "#4fb1fc";
106+
span.style.color = "#4fb1fc";
107+
search.div.firstChild.appendChild(span);
108+
search.div.dataset.mode = "ai";
109+
} else {
110+
search.div.dataset.mode = "qs";
111+
document.querySelector(".qs-escape-instructions").style.display = null;
112+
search.setResults(getDefaultResults(search.input.value.toLowerCase()));
113+
114+
getAdditionalResults(search.input);
115+
}
116+
}
117+
118+
async function getAdditionalResults(input) {
119+
let query = input.value;
120+
let results = [];
121+
122+
let projects = await getProjects(query);
123+
for (var i in projects) {
124+
results.push({
125+
text:
126+
(projects[i].title.length > 15
127+
? projects[i].title.slice(0, 15) + "..."
128+
: projects[i].title) +
129+
" by @" +
130+
(projects[i].author.username.length > 15
131+
? projects[i].author.username.slice(0, 15)
132+
: projects[i].author.username),
133+
url: `/projects/${projects[i].id.toString()}/`,
134+
type: "Project",
135+
});
136+
}
137+
138+
try {
139+
let user = await (
140+
await fetch(
141+
`https://api.scratch.mit.edu/users/${query.replaceAll("@", "")}/`
142+
)
143+
).json();
144+
if (user?.username) {
145+
results.push({
146+
text: "@" + user.username,
147+
url: `/users/${user.username}/`,
148+
type: "User",
149+
});
150+
}
151+
} catch (err) {}
152+
153+
try {
154+
let session = await feature.auth.fetch();
155+
if (session?.user?.username && "my profile".includes(query)) {
156+
results.push({
157+
text: "My Profile",
158+
url: `/users/${session.user.username}/`,
159+
});
160+
}
161+
} catch (err) {}
162+
163+
if ("scatt".includes(input.value.toLowerCase())) {
164+
results.push({
165+
text: "Scatt",
166+
type: "AI",
167+
page: "AI",
168+
});
169+
}
170+
171+
if (input.value === query) {
172+
for (var i in results) {
173+
let a = document.createElement("a");
174+
if (results[i].type === "AI") {
175+
a.addEventListener("click", function () {
176+
search.input.value = "Scatt ";
177+
search.div.firstChild.querySelector("div").innerHTML = "";
178+
inputResults();
179+
});
180+
} else {
181+
a.href = results[i].url;
182+
}
183+
a.textContent = results[i].text;
184+
185+
if (results[i].type) {
186+
let span = document.createElement("span");
187+
span.textContent = results[i].type;
188+
span.className = "qs-type";
189+
a.appendChild(span);
190+
}
191+
192+
input.parentNode.querySelector("div").prepend(a);
193+
}
194+
195+
if (
196+
[...input.parentNode.querySelectorAll("a")].find(
197+
(a) => a.textContent.toLowerCase() === query.toLowerCase()
198+
)
199+
) {
200+
input.parentNode
201+
.querySelector("div")
202+
.prepend(
203+
[...input.parentNode.querySelectorAll("a")].find(
204+
(a) => a.textContent.toLowerCase() === query.toLowerCase()
205+
)
206+
);
207+
}
208+
}
209+
}
210+
211+
async function getProjects(query) {
212+
let data = await (
213+
await fetch(
214+
`https://api.scratch.mit.edu/search/projects?limit=16&offset=0&language=en&mode=popular&q=${query
215+
.replaceAll("&", "")
216+
.replaceAll("?", "")
217+
.replaceAll("/", "")}`
218+
)
219+
).json();
220+
data.length = 2;
221+
return data;
222+
}
223+
224+
function createQuickSearch() {
225+
let div = document.createElement("div");
226+
div.className = "ste-quick-search qs-outer";
227+
div.style.display = "none";
228+
229+
let inner = document.createElement("div");
230+
inner.className = "qs-inner";
231+
div.appendChild(inner);
232+
233+
let input = document.createElement("input");
234+
input.type = "text";
235+
input.placeholder = "Search for something...";
236+
inner.appendChild(input);
237+
238+
let span = document.createElement("span");
239+
span.className = "qs-escape-instructions";
240+
span.innerHTML = `<span class='qs-key'>esc</span> to close quick search.`;
241+
inner.appendChild(span);
242+
243+
let results = document.createElement("div");
244+
inner.appendChild(results);
245+
246+
function setResults(options) {
247+
results.innerHTML = "";
248+
249+
for (var i in options) {
250+
let a = document.createElement("a");
251+
if (options[i].page === "settings") {
252+
a.addEventListener("click", function () {
253+
chrome.runtime.sendMessage(ScratchTools.id, "openSettings");
254+
});
255+
} else {
256+
a.href = options[i].url;
257+
}
258+
a.textContent = options[i].text;
259+
results.appendChild(a);
260+
}
261+
}
262+
263+
setResults(getDefaultResults());
264+
getAdditionalResults(input);
265+
266+
document.body.appendChild(div);
267+
268+
return {
269+
input,
270+
div,
271+
setResults,
272+
};
273+
}
274+
275+
function getDefaultResults(query) {
276+
let results = [
277+
{
278+
text: "New Project",
279+
url: "/projects/editor/",
280+
},
281+
{
282+
text: "Trending Projects",
283+
url: "/explore/projects/all/",
284+
},
285+
{
286+
text: "Recent Messages",
287+
url: "/messages/",
288+
},
289+
{
290+
text: "My Projects",
291+
url: "/mystuff/",
292+
},
293+
{
294+
text: "Discussion Forums",
295+
url: "/discuss/",
296+
},
297+
{
298+
text: "Account Settings",
299+
url: "/accounts/settings/",
300+
},
301+
{
302+
text: "Ideas",
303+
url: "/ideas",
304+
},
305+
{
306+
text: "ScratchTools",
307+
page: "settings",
308+
},
309+
]
310+
.filter((result) => window.location.pathname !== result.url)
311+
.filter((result) => result.text.toLowerCase().includes(query || ""));
312+
results.length = 5;
313+
return results;
314+
}
315+
}

0 commit comments

Comments
 (0)