mirror of
https://github.com/turboderp-org/exui.git
synced 2026-04-20 06:19:11 +00:00
Settings popup and persistent settings
This commit is contained in:
27
backend/settings.py
Normal file
27
backend/settings.py
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import json
|
||||
from backend.config import config_filename
|
||||
|
||||
def default_settings():
|
||||
j = {}
|
||||
j["smooth_scrolling"] = True
|
||||
j["show_stats"] = False
|
||||
return j
|
||||
|
||||
def get_settings():
|
||||
s_file = config_filename("settings.json")
|
||||
j = default_settings()
|
||||
try:
|
||||
with open(s_file, "r") as s:
|
||||
jl = json.load(s)
|
||||
j.update(jl)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return j
|
||||
|
||||
def set_settings(data_settings):
|
||||
s_file = config_filename("settings.json")
|
||||
j = data_settings
|
||||
jd = json.dumps(j, indent = 4)
|
||||
with open(s_file, "w") as outfile:
|
||||
outfile.write(jd)
|
||||
39
server.py
39
server.py
@@ -12,6 +12,7 @@ from backend.models import update_model, load_models, get_model_info, list_model
|
||||
from backend.config import set_config_dir, global_state
|
||||
from backend.sessions import list_sessions, set_session, get_session, get_default_session_settings, new_session, delete_session
|
||||
from backend.prompts import list_prompt_formats
|
||||
from backend.settings import get_settings, set_settings
|
||||
|
||||
app = Flask("ExUI")
|
||||
app.static_folder = 'static'
|
||||
@@ -167,9 +168,6 @@ def api_update_settings():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route("/api/user_input", methods=['POST'])
|
||||
def api_user_input():
|
||||
global api_lock
|
||||
@@ -183,10 +181,6 @@ def api_user_input():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route("/api/list_prompt_formats")
|
||||
def api_list_prompt_formats():
|
||||
global api_lock
|
||||
@@ -196,10 +190,6 @@ def api_list_prompt_formats():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route("/api/delete_block", methods=['POST'])
|
||||
def api_delete_block():
|
||||
global api_lock
|
||||
@@ -226,7 +216,6 @@ def api_edit_block():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
|
||||
@app.route("/api/generate", methods=['POST'])
|
||||
def api_generate():
|
||||
global api_lock
|
||||
@@ -240,9 +229,6 @@ def api_generate():
|
||||
print("->", result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route("/api/delete_session", methods=['POST'])
|
||||
def api_delete_session():
|
||||
global api_lock
|
||||
@@ -255,7 +241,6 @@ def api_delete_session():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
|
||||
@app.route("/api/remove_model", methods=['POST'])
|
||||
def api_remove_model():
|
||||
global api_lock
|
||||
@@ -268,6 +253,28 @@ def api_remove_model():
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
@app.route("/api/get_settings")
|
||||
def api_get_settings():
|
||||
global api_lock
|
||||
print("/api/get_settings")
|
||||
with api_lock:
|
||||
settings = get_settings()
|
||||
result = { "result": "ok",
|
||||
"settings": settings }
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
@app.route("/api/set_settings", methods=['POST'])
|
||||
def api_set_settings():
|
||||
global api_lock
|
||||
print("/api/set_settings")
|
||||
with api_lock:
|
||||
data = request.get_json()
|
||||
print("<-", data)
|
||||
set_settings(data["settings"])
|
||||
result = { "result": "ok" }
|
||||
print("->", result)
|
||||
return json.dumps(result) + "\n"
|
||||
|
||||
# Prepare torch
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
padding-left: 10px;
|
||||
display: var(--show-stats);
|
||||
}
|
||||
|
||||
.session-block-text p.name-assistant + .session-block-text p {
|
||||
|
||||
@@ -293,11 +293,8 @@ class SessionView {
|
||||
this.stickyScroll = true;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// smoothscroll = document.getElementById("cb-smooth-scroll").checked;
|
||||
let smoothscroll = true;
|
||||
let behavior = smoothscroll ? 'smooth' : 'auto';
|
||||
let behavior = globals.g.smoothScrolling ? 'smooth' : 'auto';
|
||||
this.chatHistory.scroll({ top: this.chatHistory.scrollHeight, behavior: behavior });
|
||||
//this.chatHistory.scroll({ top: this.chatHistory.scrollHeight, behavior: behavior });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const g = {
|
||||
// ..
|
||||
|
||||
promptFormats: null,
|
||||
smoothScrolling: true,
|
||||
|
||||
}
|
||||
|
||||
|
||||
180
static/main.js
180
static/main.js
@@ -2,191 +2,19 @@ import * as util from "./util.js";
|
||||
import * as mainmenu from "./mainmenu.js";
|
||||
import * as models from "./models.js";
|
||||
import * as chat from "./chat.js";
|
||||
import * as settings from "./settings.js";
|
||||
|
||||
var settingsPopup = new settings.SettingsPopup();
|
||||
|
||||
var mainMenu = new mainmenu.MainMenu();
|
||||
mainMenu.add("models", new mainmenu.MainTab("/static/gfx/icon_model.png", "Models", new models.Models()));
|
||||
mainMenu.add("chat", new mainmenu.MainTab("/static/gfx/icon_chat.png", "Chat", new chat.Chat()));
|
||||
mainMenu.addExtra("settings", new mainmenu.MainTab("/static/gfx/icon_settings.png", "Settings", null, () => { settingsPopup.toggle(); }));
|
||||
mainMenu.setPage("models");
|
||||
|
||||
|
||||
|
||||
|
||||
let statsvisible = false;
|
||||
let smoothscroll = true;
|
||||
|
||||
function toggleSmoothScroll() {
|
||||
smoothscroll = !smoothscroll;
|
||||
}
|
||||
|
||||
function newDiv(className = null, id = null, html = null, parent = null, editFunc = null) {
|
||||
|
||||
let nd = document.createElement('div');
|
||||
if (className) nd.className = className;
|
||||
if (id) nd.id = id;
|
||||
if (html) nd.innerHTML = html;
|
||||
if (parent) parent.appendChild(nd);
|
||||
if (editFunc) makeDivEditable(nd, editFunc);
|
||||
|
||||
return nd;
|
||||
}
|
||||
|
||||
function makeDivEditable(nd, editFunc) {
|
||||
|
||||
nd.classList.add("editable");
|
||||
nd.addEventListener('click', function() { makeEditable(nd, editFunc) });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function send(api, packet, ok_func = null, fail_func = null) {
|
||||
|
||||
fetch(api, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
if (json.hasOwnProperty("result")) {
|
||||
if (ok_func && json.result == "ok") ok_func(json);
|
||||
if (fail_func && json.result == "fail") fail_func();
|
||||
}
|
||||
else {
|
||||
//console.log(json);
|
||||
throw new Error("Bad response from server.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert("Error: " + error);
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function getTextBoxValue(id) {
|
||||
let d = document.getElementById(id);
|
||||
if (!d) return d;
|
||||
//console.log(id, d.value);
|
||||
return d.value;
|
||||
}
|
||||
|
||||
function getDivValue(id) {
|
||||
let d = document.getElementById(id);
|
||||
if (!d) return d;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function getComboBoxValue(id) {
|
||||
let d = document.getElementById(id);
|
||||
if (!d) return d;
|
||||
return d.value;
|
||||
}
|
||||
|
||||
function getCheckboxValue(id) {
|
||||
let d = document.getElementById(id);
|
||||
if (!d) return d;
|
||||
return d.checked;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function showSession(sessionID) {
|
||||
|
||||
//console.log("sessionID:", sessionID)
|
||||
|
||||
if (sessionID != "new") {
|
||||
|
||||
currentSessionUUID = sessionID;
|
||||
|
||||
let packet = {};
|
||||
packet.session_uuid = sessionID;
|
||||
|
||||
// console.log(packet);
|
||||
|
||||
fetch("/api/set_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
//console.log(json);
|
||||
showSession_(json);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
currentSessionUUID = null;
|
||||
showSession_(null);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function setChatBlockActions(nd, block)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function showHideStats() {
|
||||
statsvisible = document.getElementById("cb-show-stats").checked;
|
||||
|
||||
var divs = document.getElementsByClassName("meta");
|
||||
for(var i = 0; i < divs.length; i++) {
|
||||
if (statsvisible) divs[i].classList.remove("hidden");
|
||||
else divs[i].classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function saveFocus() {
|
||||
|
||||
let focus = {};
|
||||
focus.focusedDiv = document.activeElement;
|
||||
focus.focusedID = null;
|
||||
focus.selectionStart = -1;
|
||||
focus.selectionEnd = -1;
|
||||
|
||||
if (focus.focusedDiv) {
|
||||
focus.focusedID = focus.focusedDiv.id;
|
||||
if (focus.focusedDiv.tagName === 'INPUT' && focus.focusedDiv.type === 'text') {
|
||||
focus.selectionStart = focus.focusedDiv.selectionStart;
|
||||
focus.selectionEnd = focus.focusedDiv.selectionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(focus);
|
||||
return focus;
|
||||
}
|
||||
|
||||
function restoreFocus(focus) {
|
||||
|
||||
// console.log("restore:", focus);
|
||||
|
||||
if (focus.focusedID && focus.focusedID != "")
|
||||
{
|
||||
let focusedElement = document.getElementById(focus.focusedID);
|
||||
if (focusedElement) {
|
||||
focusedElement.focus();
|
||||
if (focus.selectionStart != -1)
|
||||
{
|
||||
focusedElement.selectionStart = focus.selectionStart;
|
||||
focusedElement.selectionEnd = focus.selectionEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
|
||||
.mainmenu-main {
|
||||
background-color: var(--background-color-menu);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mainmenu-spacer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
.mainmenu {
|
||||
background-color: var(--background-color-menu);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80px;
|
||||
padding: 0px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh);
|
||||
}
|
||||
|
||||
.mainmenu-page {
|
||||
|
||||
@@ -5,7 +5,14 @@ export class MainMenu {
|
||||
let layout = util.newVFlex("grow");
|
||||
document.body.appendChild(layout);
|
||||
|
||||
this.tabs = util.newDiv(null, "mainmenu");
|
||||
this.tabs = util.newDiv(null, "mainmenu-main");
|
||||
this.tabsTop = util.newDiv(null, "mainmenu");
|
||||
this.tabsSpacer = util.newDiv(null, "mainmenu-spacer");
|
||||
this.tabsBot = util.newDiv(null, "mainmenu");
|
||||
this.tabs.appendChild(this.tabsTop);
|
||||
this.tabs.appendChild(this.tabsSpacer);
|
||||
this.tabs.appendChild(this.tabsBot);
|
||||
|
||||
this.pages = util.newDiv(null, "mainmenu-page");
|
||||
layout.appendChild(this.tabs);
|
||||
layout.appendChild(this.pages);
|
||||
@@ -16,7 +23,7 @@ export class MainMenu {
|
||||
add(tab, control) {
|
||||
//console.log(tab, control);
|
||||
this.items.set(tab, control);
|
||||
this.tabs.appendChild(control.tab);
|
||||
this.tabsTop.appendChild(control.tab);
|
||||
if (control.page) this.pages.appendChild(control.page);
|
||||
|
||||
control.tab.addEventListener("click", () => {
|
||||
@@ -26,6 +33,14 @@ export class MainMenu {
|
||||
return control;
|
||||
}
|
||||
|
||||
addExtra(tab, control) {
|
||||
//console.log(tab, control);
|
||||
this.items.set(tab, control);
|
||||
this.tabsBot.appendChild(control.tab);
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
setPage(tab) {
|
||||
this.items.forEach((v, k) => { v.setActive(tab === k); });
|
||||
this.items.get(tab).child.onEnter();
|
||||
@@ -33,7 +48,7 @@ export class MainMenu {
|
||||
}
|
||||
|
||||
export class MainTab {
|
||||
constructor(graphic, text, child) {
|
||||
constructor(graphic, text, child, clickFunc = null) {
|
||||
this.tab = document.createElement("div");
|
||||
this.tab.className = "mainmenu-button inactive";
|
||||
this.tab.innerHTML = "<img src='" + graphic + "' width='100%' draggable='False'>" +
|
||||
@@ -43,6 +58,10 @@ export class MainTab {
|
||||
this.child = child;
|
||||
this.page = child.page;
|
||||
}
|
||||
|
||||
if (clickFunc) {
|
||||
this.tab.addEventListener("click", () => { clickFunc() });
|
||||
}
|
||||
}
|
||||
|
||||
setActive(active) {
|
||||
|
||||
@@ -333,7 +333,7 @@ export class ModelView {
|
||||
this.stats2 = new controls.LabelText("model-view-item-left", "Dimension", "model-view-item-right", this.modelInfo_compiled, "stats2");
|
||||
this.stats3 = new controls.LabelText("model-view-item-left", "Heads", "model-view-item-right", this.modelInfo_compiled, "stats3");
|
||||
this.stats4 = new controls.LabelText("model-view-item-left", "Vocab size", "model-view-item-right", this.modelInfo_compiled, "stats4");
|
||||
this.stats5 = new controls.LabelText("model-view-item-left", "Default seq. length", "model-view-item-right", this.modelInfo_compiled, "stats5");
|
||||
this.stats5 = new controls.LabelText("model-view-item-left", "Default context length", "model-view-item-right", this.modelInfo_compiled, "stats5");
|
||||
this.element_model.appendChild(this.stats1.element);
|
||||
this.element_model.appendChild(this.stats2.element);
|
||||
this.element_model.appendChild(this.stats3.element);
|
||||
|
||||
40
static/settings.css
Normal file
40
static/settings.css
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
.settings-float {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 90px;
|
||||
width: 228px;
|
||||
height: 65px;
|
||||
background: var(--background-color-view);
|
||||
border-radius: 10px;
|
||||
box-shadow: 5px 5px 15px rgba(0,0,0,0.2);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
z-index: 999;
|
||||
border: 1px solid var(--background-color-view);
|
||||
}
|
||||
|
||||
.settings-float::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 30%;
|
||||
border-width: 15px 22px 15px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent var(--background-color-view); transparent transparent;
|
||||
}
|
||||
|
||||
.checkbox-sc {
|
||||
color: var(--textcolor-menu);
|
||||
align-items: center;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.checkbox-sc:hover {
|
||||
filter: brightness(140%);
|
||||
cursor: pointer;
|
||||
}
|
||||
67
static/settings.js
Normal file
67
static/settings.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as util from "./util.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as globals from "./globals.js";
|
||||
|
||||
export class SettingsPopup {
|
||||
constructor() {
|
||||
this.element = util.newDiv(null, "settings-float");
|
||||
this.element.classList.add("hidden");
|
||||
document.body.appendChild(this.element);
|
||||
|
||||
this.populated = false;
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.element.classList.contains("hidden")) {
|
||||
this.element.classList.remove("hidden");
|
||||
} else {
|
||||
this.element.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
fetch("/api/get_settings")
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
this.settings = response.settings;
|
||||
this.populate();
|
||||
});
|
||||
}
|
||||
|
||||
populate() {
|
||||
if (this.populated) return;
|
||||
|
||||
this.s_smoothScrolling = new controls.CheckboxLabel("checkbox-sc noselect", "Smooth scrolling", this.settings, "smooth_scrolling", () => { this.saveSettings(); });
|
||||
this.s_showStats = new controls.CheckboxLabel("checkbox-sc noselect", "Display stats", this.settings, "show_stats", () => { this.saveSettings(); });
|
||||
this.element.appendChild(this.s_smoothScrolling.element);
|
||||
this.element.appendChild(this.s_showStats.element);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.s_smoothScrolling.refresh();
|
||||
this.s_showStats.refresh();
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
this.applyGlobals();
|
||||
|
||||
let packet = {}
|
||||
packet.settings = this.settings;
|
||||
fetch("/api/set_settings", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) })
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
});
|
||||
}
|
||||
|
||||
applyGlobals() {
|
||||
// console.log(this.settings);
|
||||
globals.g.smoothScrolling = this.settings.smooth_scrolling;
|
||||
|
||||
let r = document.querySelector(':root');
|
||||
r.style.setProperty("--show-stats", this.settings.show_stats ? "flex" : "none");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,6 +43,8 @@
|
||||
--slider-thumb-color: rgb(135, 150, 155);
|
||||
--slider-track-color: rgb(77, 77, 77);
|
||||
--slider-hover-color: rgb(88, 88, 88);
|
||||
|
||||
--show-stats: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -97,23 +99,3 @@ svg {
|
||||
svg.active {
|
||||
fill: var(--iconcolor-active);
|
||||
}
|
||||
|
||||
.checkbox-sc {
|
||||
align-items: center;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.checkbox-sc:hover {
|
||||
filter: brightness(130%);
|
||||
}
|
||||
|
||||
.checkbox-sc label {
|
||||
color: var(--textcolor-head);
|
||||
cursor: pointer;
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -144,3 +144,44 @@ export function makeEditable(div, validateFunc, onValueChange, onExit, in_text =
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
function saveFocus() {
|
||||
|
||||
let focus = {};
|
||||
focus.focusedDiv = document.activeElement;
|
||||
focus.focusedID = null;
|
||||
focus.selectionStart = -1;
|
||||
focus.selectionEnd = -1;
|
||||
|
||||
if (focus.focusedDiv) {
|
||||
focus.focusedID = focus.focusedDiv.id;
|
||||
if (focus.focusedDiv.tagName === 'INPUT' && focus.focusedDiv.type === 'text') {
|
||||
focus.selectionStart = focus.focusedDiv.selectionStart;
|
||||
focus.selectionEnd = focus.focusedDiv.selectionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(focus);
|
||||
return focus;
|
||||
}
|
||||
|
||||
function restoreFocus(focus) {
|
||||
|
||||
// console.log("restore:", focus);
|
||||
|
||||
if (focus.focusedID && focus.focusedID != "")
|
||||
{
|
||||
let focusedElement = document.getElementById(focus.focusedID);
|
||||
if (focusedElement) {
|
||||
focusedElement.focus();
|
||||
if (focus.selectionStart != -1)
|
||||
{
|
||||
focusedElement.selectionStart = focus.selectionStart;
|
||||
focusedElement.selectionEnd = focus.selectionEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='slider.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='controls.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='mainmenu.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='settings.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='spinner.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='models.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='chat.css') }}">
|
||||
@@ -22,6 +23,7 @@
|
||||
<script type="module" src="{{ url_for('static', filename='overlay.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='controls.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='mainmenu.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='settings.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='models.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='chat.js') }}"></script>
|
||||
<script type="module" src="{{ url_for('static', filename='chatsettings.js') }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user