feat: webui use querystring as settings (#814)

This commit is contained in:
sigoden 2024-08-31 17:53:57 +08:00 committed by GitHub
parent a204a4b189
commit ed242c65f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 81 deletions

View File

@ -392,9 +392,9 @@
<template x-for="(chat, index) in chats" :key="index">
<div class="chat-panel">
<div class="chat-header">
<select x-cloak id="model" x-model="chat.model_id" @change="handleModelIdChange">
<template x-for="model in models" :key="model.id">
<option :value="model.id" :selected="model.id == chat.model_id" x-text="model.id"></option>
<select x-cloak id="model" x-model="chat.model" @change="handleModelChange">
<template x-for="model in chatModels" :key="model.id">
<option :value="model.id" :selected="model.id == chat.model" x-text="model.id"></option>
</template>
</select>
</div>
@ -548,13 +548,12 @@
<div id="toast" class="toast"></div>
</div>
<script>
const QUERY = parseQueryString(location.search);
const QUERY = parseQueryString();
const NUM = parseInt(QUERY.num) || 2
const API_BASE = QUERY.api_base || "./v1";
const API_KEY = QUERY.api_key || "";
const CHAT_COMPLETIONS_URL = API_BASE + "/chat/completions";
const MODELS_API = API_BASE + "/models";
const MODEL_IDS_STORAGE_KEY = "__model_ids__";
document.addEventListener("alpine:init", () => {
setupMarked();
@ -568,12 +567,12 @@
let msgIdx = 0;
Alpine.data("app", () => ({
models: [],
chatModels: [],
input: "",
images: [],
asking: 0,
chats: Array.from(Array(NUM)).map(_ => ({
model_id: "",
model: "",
messages: [],
hoveredMessageIndex: null,
askAbortController: null,
@ -584,20 +583,20 @@
async init() {
try {
const models = await fetchJSON(MODELS_API);
this.models = models.filter(v => !v.type || v.type === "chat");
this.chatModels = models.filter(v => !v.type || v.type === "chat");
} catch (err) {
toast("No available model");
console.error("Failed to load models", err);
}
let model_ids = []
try {
model_ids = JSON.parse(localStorage.getItem(MODEL_IDS_STORAGE_KEY)) || [];
} catch { }
let models = []
if (QUERY.models) {
models = QUERY.models.split(",");
}
$chatPanels = document.querySelectorAll('.chat-panel');
$scrollToBottomBtns = document.querySelectorAll('.scroll-to-bottom-btn');
const offsets = calculateOffsets(NUM);
for (let i = 0; i < NUM; i++) {
this.chats[i].model_id = model_ids[i] || "default";
this.chats[i].model = models[i] || "default";
$chatPanels[i].style.width = (100 / NUM) + '%';
if (i > 0) {
$chatPanels[i].style.borderLeft = '1px solid var(--border-primary)';
@ -611,7 +610,7 @@
},
get supportsVision() {
return this.chats.every(v => !!retrieveModel(this.models, v.model_id)?.supports_vision)
return this.chats.every(v => !!retrieveModel(this.chatModels, v.model)?.supports_vision)
},
handleAsk() {
@ -697,9 +696,8 @@
}
},
handleModelIdChange() {
let model_ids = this.chats.map(v => v.model_id);
localStorage.setItem(MODEL_IDS_STORAGE_KEY, JSON.stringify(model_ids));
handleModelChange() {
this.updateUrl();
},
handleScrollChatBody(event, index) {
@ -772,6 +770,13 @@
event.target.value = "";
},
updateUrl() {
const newUrl = new URL(location.href);
const models = this.chats.map(v => v.model).join(",");
newUrl.searchParams.set("models", models);
history.replaceState(null, '', newUrl.toString());
},
autoHeightChatPanel() {
const height = $inputPanel.offsetHeight;
for (let i = 0; i < this.chats.length; i++) {
@ -852,11 +857,11 @@
}
}
const body = {
model: chat.model_id,
model: chat.model,
messages: messages,
stream: true,
};
const { max_output_token, require_max_tokens } = retrieveModel(this.models, chat.model_id);
const { max_output_token, require_max_tokens } = retrieveModel(this.chatModels, chat.model);
if (!body["max_tokens"] && require_max_tokens) {
body["max_tokens"] = max_output_token;
};
@ -1005,20 +1010,13 @@
return input.replace(/([&<>'"])/g, char => escapeMap[char]);
}
function parseQueryString(queryString) {
const params = {};
if (!queryString) {
return params;
}
const queries = queryString[0] === '?' ? queryString.substring(1) : queryString;
const pairs = queries.split('&');
pairs.forEach(pair => {
const [key, value] = pair.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value ? value.replace(/\+/g, ' ') : '');
function parseQueryString() {
const params = new URLSearchParams(location.search);
const queryObject = {};
params.forEach((value, key) => {
queryObject[key] = value;
});
return params;
return queryObject;
}
function chunkArray(array, chunkSize) {

View File

@ -503,7 +503,7 @@
<div class="settings">
<div class="control">
<label for="role">Role</label>
<select id="role" x-model="settings.role" :disabled="sessionMode" @change="handleRoleChange">
<select id="role" x-model="settings.role" :disabled="sessionMode">
<template x-for="role in roles">
<option :value="role.name" :selected="role.name == settings.role" x-text="role.name"></option>
</template>
@ -517,7 +517,7 @@
<div class="control">
<label for="max_output_tokens"
x-text="'Max Output Tokens' + (currentModel.max_output_token ? ' [1, ' + currentModel.max_output_token + ']' : '')">Max
x-text="'Max Output Tokens' + (modelData.max_output_token ? ' [1, ' + modelData.max_output_token + ']' : '')">Max
Output Tokens</label>
<input type="number" id="max_output_tokens" x-model="settings.max_output_tokens">
</div>
@ -536,9 +536,9 @@
</div>
<div class="main-panel" x-ref="main-panel">
<div class="chat-header">
<select id="model" x-model="settings.model_id">
<select id="model" x-model="settings.model">
<template x-for="model in models" :key="model.id">
<option :value="model.id" :selected="model.id == settings.model_id" x-text="model.id"></option>
<option :value="model.id" :selected="model.id == settings.model" x-text="model.id"></option>
</template>
</select>
<div class="toolbar">
@ -671,7 +671,7 @@
</template>
<template x-if="!asking">
<div class="input-toolbox">
<div class="image-btn" x-show="currentModel.supports_vision">
<div class="image-btn" x-show="modelData.supports_vision">
<input type="file" multiple accept=".jpg,.jpeg,.png,.webp" @change="handleImageUpload">
<svg fill="currentColor" viewBox="0 0 16 16">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0" />
@ -694,13 +694,12 @@
<div id="toast" class="toast"></div>
</div>
<script>
const QUERY = parseQueryString(location.search);
const QUERY = parseQueryString();
const API_BASE = QUERY.api_base || "./v1";
const API_KEY = QUERY.api_key || "";
const CHAT_COMPLETIONS_URL = API_BASE + "/chat/completions";
const MODELS_API = API_BASE + "/models";
const ROLES_API = API_BASE + "/roles";
const SETTINGS_STORAGE_KEY = "__settings__";
document.addEventListener("alpine:init", () => {
setupMarked();
@ -710,28 +709,18 @@
function setupApp() {
let msgIdx = 0;
let defaultSettings = {
model_id: "default",
role: "",
model: QUERY.model || "default",
role: QUERY.role || "",
prompt: "",
max_output_tokens: null,
temperature: null,
top_p: null,
max_output_tokens: parseInt(QUERY.max_output_tokens) || null,
temperature: QUERY.temperature ? parseFloat(QUERY.temperature) : null,
top_p: QUERY.top_p ? parseFloat(QUERY.top_p) : null,
};
try {
const persistSettings = JSON.parse(localStorage.getItem(SETTINGS_STORAGE_KEY)) || {};
const sanitizedPersistSettings = Object.keys(defaultSettings).reduce((acc, cur) => {
if (persistSettings.hasOwnProperty(cur)) {
acc[cur] = persistSettings[cur];
}
return acc;
}, {});
defaultSettings = { ...defaultSettings, ...sanitizedPersistSettings };
} catch { }
Alpine.data("app", () => ({
models: [],
roles: [{ name: "", prompt: "" }],
currentModel: {},
modelData: {},
messages: [],
hoveredMessageIndex: null,
input: "",
@ -757,15 +746,19 @@
}).catch(() => { }),
])
this.$watch("input", () => this.autosizeInput(this.$refs.input));
this.$watch("settings", () => {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(this.settings));
});
this.$watch("settings.model_id", () => this.handleModelIdChange());
this.handleModelIdChange();
},
handleRoleChange(event) {
this.settings.prompt = this.roles.find(role => role.name === event.target.value).prompt;
this.$watch("settings", () => this.updateUrl());
this.$watch("settings.model", () => this.handleModelChange());
if (this.models.find(model => model.id === this.settings.model)) {
this.handleModelChange();
} else {
this.settings.model = "default";
}
this.$watch("settings.role", () => this.handleRoleChange())
if (this.roles.find(role => role.name === this.settings.role)) {
this.handleRoleChange();
} else {
this.settings.role = "";
}
},
handleAsk() {
@ -840,8 +833,12 @@
this.askAbortController?.abort();
},
handleModelIdChange() {
this.currentModel = retrieveModel(this.models, this.settings.model_id);
handleModelChange() {
this.modelData = retrieveModel(this.models, this.settings.model);
},
handleRoleChange() {
this.settings.prompt = this.roles.find(role => role.name === this.settings.role).prompt;
},
handleScrollChatBody(event) {
@ -923,6 +920,18 @@
event.target.value = "";
},
updateUrl() {
const newUrl = new URL(location.href);
["model", "role", "max_output_tokens", "temperature", "top_p"].forEach(key => {
if (this.settings[key] || typeof this.settings[key] === "number") {
newUrl.searchParams.set(key, this.settings[key]);
} else {
newUrl.searchParams.delete(key);
}
});
history.replaceState(null, '', newUrl.toString());
},
autoScrollChatBodyToBottom() {
if (this.shouldScrollChatBodyToBottom) {
let $chatBody = this.$refs["chat-body"];
@ -1022,7 +1031,7 @@
}
}
const body = {
model: this.settings.model_id,
model: this.settings.model,
messages: messages,
stream: true,
};
@ -1031,7 +1040,7 @@
body[body_key || setting_key] = this.settings[setting_key];
}
});
const { max_output_token, require_max_tokens } = this.currentModel;
const { max_output_token, require_max_tokens } = this.modelData;
if (!body["max_tokens"] && require_max_tokens) {
body["max_tokens"] = max_output_token;
};
@ -1228,20 +1237,13 @@
return input.replace(/([&<>'"])/g, char => escapeMap[char]);
}
function parseQueryString(queryString) {
const params = {};
if (!queryString) {
return params;
}
const queries = queryString[0] === '?' ? queryString.substring(1) : queryString;
const pairs = queries.split('&');
pairs.forEach(pair => {
const [key, value] = pair.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value ? value.replace(/\+/g, ' ') : '');
function parseQueryString() {
const params = new URLSearchParams(location.search);
const queryObject = {};
params.forEach((value, key) => {
queryObject[key] = value;
});
return params;
return queryObject;
}
function chunkArray(array, chunkSize) {