mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-09 01:09:54 +00:00
dropdowns
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: "Lato", sans-serif;
|
font-family: "Work Sans", sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: #181926;
|
background: #181926;
|
||||||
color: #cad3f5;
|
color: #cad3f5;
|
||||||
@@ -284,6 +284,7 @@ body {
|
|||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown,
|
||||||
textarea,
|
textarea,
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
@@ -307,6 +308,8 @@ button {
|
|||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
background: #363a4f;
|
background: #363a4f;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat button {
|
#chat button {
|
||||||
|
74
static/css/dropdown.css
Normal file
74
static/css/dropdown.css
Normal file
@@ -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;
|
||||||
|
}
|
@@ -2,7 +2,6 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
color: #CAD3F5;
|
color: #CAD3F5;
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h1,
|
.markdown h1,
|
||||||
|
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@100..900&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="css/catppuccin.css" rel="stylesheet" />
|
<link href="css/catppuccin.css" rel="stylesheet" />
|
||||||
|
<link href="css/dropdown.css" rel="stylesheet" />
|
||||||
<link href="css/markdown.css" rel="stylesheet" />
|
<link href="css/markdown.css" rel="stylesheet" />
|
||||||
<link href="css/chat.css" rel="stylesheet" />
|
<link href="css/chat.css" rel="stylesheet" />
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<label for="model" title="Model"></label>
|
<label for="model" title="Model"></label>
|
||||||
<select id="model"></select>
|
<select id="model" data-searchable></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<label for="prompt" title="Prompt"></label>
|
<label for="prompt" title="Prompt"></label>
|
||||||
|
@@ -585,5 +585,8 @@
|
|||||||
scroll(!interacted);
|
scroll(!interacted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dropdown($role);
|
||||||
|
dropdown($prompt);
|
||||||
|
|
||||||
loadModels().then(restore);
|
loadModels().then(restore);
|
||||||
})();
|
})();
|
||||||
|
@@ -3,17 +3,22 @@
|
|||||||
#_select;
|
#_select;
|
||||||
#_dropdown;
|
#_dropdown;
|
||||||
#_selected;
|
#_selected;
|
||||||
|
#_search;
|
||||||
|
|
||||||
|
#search = false;
|
||||||
#selected = false;
|
#selected = false;
|
||||||
#options = [];
|
#options = [];
|
||||||
|
|
||||||
constructor(el) {
|
constructor(el) {
|
||||||
this.#_select = el;
|
this.#_select = el;
|
||||||
|
|
||||||
|
this.#search = "searchable" in el.dataset;
|
||||||
|
|
||||||
this.#_select.querySelectorAll("option").forEach((option) => {
|
this.#_select.querySelectorAll("option").forEach((option) => {
|
||||||
this.#options.push({
|
this.#options.push({
|
||||||
value: option.value,
|
value: option.value,
|
||||||
label: option.textContent.trim(),
|
label: option.textContent,
|
||||||
|
search: searchable(option.textContent),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,33 +52,72 @@
|
|||||||
// dropdown
|
// dropdown
|
||||||
this.#_dropdown = make("div", "dropdown");
|
this.#_dropdown = make("div", "dropdown");
|
||||||
|
|
||||||
this.#_dropdown.addEventListener("click", () => {
|
|
||||||
this.#_dropdown.classList.add("open");
|
|
||||||
});
|
|
||||||
|
|
||||||
// selected item
|
// selected item
|
||||||
this.#_selected = make("div", "selected");
|
this.#_selected = make("div", "selected");
|
||||||
|
|
||||||
|
this.#_selected.addEventListener("click", () => {
|
||||||
|
this.#_dropdown.classList.toggle("open");
|
||||||
|
});
|
||||||
|
|
||||||
this.#_dropdown.appendChild(this.#_selected);
|
this.#_dropdown.appendChild(this.#_selected);
|
||||||
|
|
||||||
// option wrapper
|
// content
|
||||||
const _options = make("div", "options");
|
const _content = make("div", "cont");
|
||||||
|
|
||||||
this.#_dropdown.appendChild(_options);
|
this.#_dropdown.appendChild(_content);
|
||||||
|
|
||||||
|
// option wrapper
|
||||||
|
const _options = make("div", "opts");
|
||||||
|
|
||||||
|
_content.appendChild(_options);
|
||||||
|
|
||||||
// options
|
// options
|
||||||
for (const option of this.#options) {
|
for (const option of this.#options) {
|
||||||
const _opt = make("div", "option");
|
const _opt = make("div", "opt");
|
||||||
|
|
||||||
_opt.textContent = option.label;
|
_opt.textContent = option.label;
|
||||||
|
|
||||||
_opt.addEventListener("click", () => {
|
_opt.addEventListener("click", () => {
|
||||||
this.#set(option.value);
|
this.#set(option.value);
|
||||||
|
|
||||||
|
this.#_dropdown.classList.remove("open");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_options.appendChild(_opt);
|
||||||
|
|
||||||
option.el = _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
|
// add to dom
|
||||||
this.#_select.after(this.#_dropdown);
|
this.#_select.after(this.#_dropdown);
|
||||||
|
|
||||||
@@ -92,9 +136,23 @@
|
|||||||
this.#_selected.textContent = selection.label;
|
this.#_selected.textContent = selection.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
#set(value) {
|
#filter() {
|
||||||
console.log("value", value);
|
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);
|
const index = this.#options.findIndex((option) => option.value === value);
|
||||||
|
|
||||||
if (this.#selected === index) {
|
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) => {
|
document.body.addEventListener("click", (event) => {
|
||||||
const clicked = event.target.closest(".dropdown");
|
const clicked = event.target.closest(".dropdown");
|
||||||
|
|
||||||
|
102
static/js/lib.js
102
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 = `<div class="pre-header">${escapeHtml(code.lang)}<button class="pre-copy" title="Copy code contents"></button></div>`;
|
|
||||||
|
|
||||||
return `<pre>${header}<code>${code.text}</code></pre>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
image(image) {
|
|
||||||
const { href } = image;
|
|
||||||
|
|
||||||
const id = `i_${btoa(href).replace(/=/g, "")}`,
|
|
||||||
style = prepareImage(id, href) || "";
|
|
||||||
|
|
||||||
return `<div class="image ${id}" style="${style}"></div>`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
function storeValue(key, value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
|
Reference in New Issue
Block a user