diff --git a/static/css/chat.css b/static/css/chat.css
index 1a8e934..c6c7b74 100644
--- a/static/css/chat.css
+++ b/static/css/chat.css
@@ -30,7 +30,7 @@
html,
body {
- font-family: "Lato", sans-serif;
+ font-family: "Work Sans", sans-serif;
font-size: 15px;
background: #181926;
color: #cad3f5;
@@ -284,6 +284,7 @@ body {
padding-bottom: 30px;
}
+.dropdown,
textarea,
button,
input,
@@ -307,6 +308,8 @@ button {
input,
select {
background: #363a4f;
+ padding: 2px 5px;
+ font-size: 14px;
}
#chat button {
diff --git a/static/css/dropdown.css b/static/css/dropdown.css
new file mode 100644
index 0000000..8db5fd4
--- /dev/null
+++ b/static/css/dropdown.css
@@ -0,0 +1,74 @@
+.dropdown {
+ position: relative;
+ user-select: none;
+ background: #363a4f;
+ padding: 2px 5px;
+ font-size: 14px;
+ white-space: nowrap;
+}
+
+.dropdown .selected {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ cursor: pointer;
+}
+
+.dropdown .selected::after {
+ content: "";
+ display: block;
+ width: 16px;
+ height: 16px;
+ background-image: url(icons/chevron.svg);
+ background-position: center;
+ background-size: contain;
+ background-repeat: no-repeat;
+ transform: rotate(180deg);
+}
+
+.dropdown .cont {
+ display: flex;
+ position: absolute;
+ bottom: calc(100% + 4px);
+ left: 0;
+ flex-direction: column;
+ background: #363a4f;
+ max-height: 250px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
+ border: 1px solid #494d64;
+}
+
+.dropdown:not(.open) .cont {
+ display: none;
+}
+
+.dropdown .opts {
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+
+.dropdown .opt {
+ padding: 4px 6px;
+ cursor: pointer;
+ background: #363a4f;
+}
+
+.dropdown .opt:nth-child(2n+1) {
+ background: #32354a;
+}
+
+.dropdown .opt:hover {
+ background: #2c2f44;
+}
+
+.dropdown .opt.filtered {
+ display: none;
+}
+
+.dropdown .search {
+ background: #2a2e41;
+ border-top: 2px solid #494d64;
+ font-size: 15px;
+ padding: 4px 6px;
+}
\ No newline at end of file
diff --git a/static/css/markdown.css b/static/css/markdown.css
index edd249f..6d34145 100644
--- a/static/css/markdown.css
+++ b/static/css/markdown.css
@@ -2,7 +2,6 @@
font-size: 15px;
line-height: 23px;
color: #CAD3F5;
- font-family: system-ui, sans-serif;
}
.markdown h1,
diff --git a/static/index.html b/static/index.html
index c4f828b..567920e 100644
--- a/static/index.html
+++ b/static/index.html
@@ -6,9 +6,10 @@
-
+
+
@@ -36,7 +37,7 @@
diff --git a/static/js/chat.js b/static/js/chat.js
index 6fe2243..5961872 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -585,5 +585,8 @@
scroll(!interacted);
});
+ dropdown($role);
+ dropdown($prompt);
+
loadModels().then(restore);
})();
diff --git a/static/js/dropdown.js b/static/js/dropdown.js
index a7666ae..059a882 100644
--- a/static/js/dropdown.js
+++ b/static/js/dropdown.js
@@ -3,17 +3,22 @@
#_select;
#_dropdown;
#_selected;
+ #_search;
+ #search = false;
#selected = false;
#options = [];
constructor(el) {
this.#_select = el;
+ this.#search = "searchable" in el.dataset;
+
this.#_select.querySelectorAll("option").forEach((option) => {
this.#options.push({
value: option.value,
- label: option.textContent.trim(),
+ label: option.textContent,
+ search: searchable(option.textContent),
});
});
@@ -47,33 +52,72 @@
// dropdown
this.#_dropdown = make("div", "dropdown");
- this.#_dropdown.addEventListener("click", () => {
- this.#_dropdown.classList.add("open");
- });
-
// selected item
this.#_selected = make("div", "selected");
+ this.#_selected.addEventListener("click", () => {
+ this.#_dropdown.classList.toggle("open");
+ });
+
this.#_dropdown.appendChild(this.#_selected);
- // option wrapper
- const _options = make("div", "options");
+ // content
+ const _content = make("div", "cont");
- this.#_dropdown.appendChild(_options);
+ this.#_dropdown.appendChild(_content);
+
+ // option wrapper
+ const _options = make("div", "opts");
+
+ _content.appendChild(_options);
// options
for (const option of this.#options) {
- const _opt = make("div", "option");
+ const _opt = make("div", "opt");
_opt.textContent = option.label;
_opt.addEventListener("click", () => {
this.#set(option.value);
+
+ this.#_dropdown.classList.remove("open");
});
+ _options.appendChild(_opt);
+
option.el = _opt;
}
+ // live search (if enabled)
+ if (this.#search) {
+ this.#_search = make("input", "search");
+
+ this.#_search.type = "text";
+ this.#_search.placeholder = "Search...";
+
+ this.#_search.addEventListener("input", () => {
+ this.#filter();
+ });
+
+ this.#_search.addEventListener("keydown", (event) => {
+ if (event.key !== "Escape") {
+ return;
+ }
+
+ if (this.#_search.value) {
+ this.#_search.value = "";
+
+ this.#_search.dispatchEvent(new Event("input"));
+
+ return;
+ }
+
+ this.#_dropdown.classList.remove("open");
+ });
+
+ _content.appendChild(this.#_search);
+ }
+
// add to dom
this.#_select.after(this.#_dropdown);
@@ -92,9 +136,23 @@
this.#_selected.textContent = selection.label;
}
- #set(value) {
- console.log("value", value);
+ #filter() {
+ if (!this.#_search) {
+ return;
+ }
+ const query = searchable(this.#_search.value);
+
+ for (const option of this.#options) {
+ if (query && !option.search.includes(query)) {
+ option.el.classList.add("filtered");
+ } else {
+ option.el.classList.remove("filtered");
+ }
+ }
+ }
+
+ #set(value) {
const index = this.#options.findIndex((option) => option.value === value);
if (this.#selected === index) {
@@ -107,6 +165,16 @@
}
}
+ function searchable(text) {
+ // lowercase
+ text = text.toLowerCase();
+
+ // only alpha-num
+ text = text.replace(/[^\w]/g, "");
+
+ return text.trim();
+ }
+
document.body.addEventListener("click", (event) => {
const clicked = event.target.closest(".dropdown");
diff --git a/static/js/lib.js b/static/js/lib.js
index 3678546..8ed5626 100644
--- a/static/js/lib.js
+++ b/static/js/lib.js
@@ -1,105 +1,3 @@
-(() => {
- const timeouts = new WeakMap(),
- images = {};
-
- marked.use({
- async: false,
- breaks: false,
- gfm: true,
- pedantic: false,
-
- walkTokens: (token) => {
- const { type, lang, text } = token;
-
- if (type !== "code") {
- return;
- }
-
- let code;
-
- if (lang && hljs.getLanguage(lang)) {
- code = hljs.highlight(text, {
- language: lang,
- });
- } else {
- code = hljs.highlightAuto(text);
- }
-
- token.escaped = true;
- token.text = code.value;
- },
-
- renderer: {
- code(code) {
- const header = ``;
-
- return `
${header}${code.text}
`;
- },
-
- image(image) {
- const { href } = image;
-
- const id = `i_${btoa(href).replace(/=/g, "")}`,
- style = prepareImage(id, href) || "";
-
- return `
`;
- },
- },
- });
-
- function prepareImage(id, href) {
- if (href in images) {
- return images[href];
- }
-
- images[href] = false;
-
- const image = new Image();
-
- image.addEventListener("load", () => {
- const style = `aspect-ratio:${image.naturalWidth}/${image.naturalHeight};width:${image.naturalWidth}px;background-image:url(${href})`;
-
- images[href] = style;
-
- document.querySelectorAll(`.image.${id}`).forEach((img) => {
- img.setAttribute("style", style);
- });
- });
-
- image.addEventListener("error", () => {
- console.error(`Failed to load image: ${href}`);
- });
-
- image.src = href;
-
- return false;
- }
-
- document.body.addEventListener("click", (event) => {
- const button = event.target,
- header = button.closest(".pre-header"),
- pre = header?.closest("pre"),
- code = pre?.querySelector("code");
-
- if (!code) {
- return;
- }
-
- clearTimeout(timeouts.get(pre));
-
- navigator.clipboard.writeText(code.textContent.trim());
-
- button.classList.add("copied");
-
- timeouts.set(
- pre,
- setTimeout(() => {
- button.classList.remove("copied");
- }, 1000),
- );
- });
-})();
-
function storeValue(key, value) {
if (!value) {
localStorage.removeItem(key);