mirror of
https://github.com/sigoden/aichat
synced 2024-11-12 01:10:43 +00:00
feat: webui use querystring as settings (#814)
This commit is contained in:
parent
a204a4b189
commit
ed242c65f0
@ -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) {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user