306 lines
14 KiB
JavaScript
306 lines
14 KiB
JavaScript
// frontend/js/pages/keys/requestSettingsModal.js
|
|
import { modalManager } from '../../components/ui.js';
|
|
|
|
export default class RequestSettingsModal {
|
|
constructor({ onSave }) {
|
|
this.modalId = 'request-settings-modal';
|
|
this.modal = document.getElementById(this.modalId);
|
|
this.onSave = onSave; // 注入保存回調函數
|
|
if (!this.modal) {
|
|
throw new Error(`Modal with id "${this.modalId}" not found.`);
|
|
}
|
|
// 映射所有內部DOM元素
|
|
this.elements = {
|
|
saveBtn: document.getElementById('request-settings-save-btn'),
|
|
customHeadersContainer: document.getElementById('CUSTOM_HEADERS_container'),
|
|
addCustomHeaderBtn: document.getElementById('addCustomHeaderBtn'),
|
|
streamOptimizerEnabled: document.getElementById('STREAM_OPTIMIZER_ENABLED'),
|
|
streamingSettingsPanel: document.getElementById('streaming-settings-panel'),
|
|
streamMinDelay: document.getElementById('STREAM_MIN_DELAY'),
|
|
streamMaxDelay: document.getElementById('STREAM_MAX_DELAY'),
|
|
streamShortTextThresh: document.getElementById('STREAM_SHORT_TEXT_THRESHOLD'),
|
|
streamLongTextThresh: document.getElementById('STREAM_LONG_TEXT_THRESHOLD'),
|
|
streamChunkSize: document.getElementById('STREAM_CHUNK_SIZE'),
|
|
fakeStreamEnabled: document.getElementById('FAKE_STREAM_ENABLED'),
|
|
fakeStreamInterval: document.getElementById('FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS'),
|
|
toolsCodeExecutionEnabled: document.getElementById('TOOLS_CODE_EXECUTION_ENABLED'),
|
|
urlContextEnabled: document.getElementById('URL_CONTEXT_ENABLED'),
|
|
showSearchLink: document.getElementById('SHOW_SEARCH_LINK'),
|
|
showThinkingProcess: document.getElementById('SHOW_THINKING_PROCESS'),
|
|
safetySettingsContainer: document.getElementById('SAFETY_SETTINGS_container'),
|
|
addSafetySettingBtn: document.getElementById('addSafetySettingBtn'),
|
|
configOverrides: document.getElementById('group-config-overrides'),
|
|
};
|
|
this._initEventListeners();
|
|
}
|
|
// --- 公共 API ---
|
|
/**
|
|
* 打開模態框並填充數據
|
|
* @param {object} data - 用於填充表單的數據
|
|
*/
|
|
open(data) {
|
|
this._populateForm(data);
|
|
modalManager.show(this.modalId);
|
|
}
|
|
/**
|
|
* 關閉模態框
|
|
*/
|
|
close() {
|
|
modalManager.hide(this.modalId);
|
|
}
|
|
// --- 內部事件與邏輯 ---
|
|
_initEventListeners() {
|
|
// 事件委託,處理動態添加元素的移除
|
|
this.modal.addEventListener('click', (e) => {
|
|
const removeBtn = e.target.closest('.remove-btn');
|
|
if (removeBtn) {
|
|
removeBtn.parentElement.remove();
|
|
}
|
|
});
|
|
if (this.elements.addCustomHeaderBtn) {
|
|
this.elements.addCustomHeaderBtn.addEventListener('click', () => this.addCustomHeaderItem());
|
|
}
|
|
if (this.elements.addSafetySettingBtn) {
|
|
this.elements.addSafetySettingBtn.addEventListener('click', () => this.addSafetySettingItem());
|
|
}
|
|
if (this.elements.saveBtn) {
|
|
this.elements.saveBtn.addEventListener('click', this._handleSave.bind(this));
|
|
}
|
|
if (this.elements.streamOptimizerEnabled) {
|
|
this.elements.streamOptimizerEnabled.addEventListener('change', (e) => {
|
|
this._toggleStreamingPanel(e.target.checked);
|
|
});
|
|
}
|
|
// --- 完整的、統一的關閉邏輯 ---
|
|
const closeAction = () => {
|
|
// 此處無需 _reset(),因為每次 open 都會重新 populate
|
|
modalManager.hide(this.modalId);
|
|
};
|
|
// 綁定所有帶有 data-modal-close 屬性的按鈕
|
|
const closeTriggers = this.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
|
|
closeTriggers.forEach(trigger => {
|
|
trigger.addEventListener('click', closeAction);
|
|
});
|
|
// 綁定點擊模態框背景遮罩層的事件
|
|
this.modal.addEventListener('click', (event) => {
|
|
if (event.target === this.modal) {
|
|
closeAction();
|
|
}
|
|
});
|
|
}
|
|
|
|
async _handleSave() {
|
|
const data = this._collectFormData();
|
|
if (this.onSave) {
|
|
try {
|
|
if (this.elements.saveBtn) {
|
|
this.elements.saveBtn.disabled = true;
|
|
this.elements.saveBtn.textContent = 'Saving...';
|
|
}
|
|
// 調用注入的回調函數,將數據傳遞給頁面控制器處理
|
|
await this.onSave(data);
|
|
this.close();
|
|
} catch (error) {
|
|
console.error("Failed to save request settings:", error);
|
|
alert(`保存失敗: ${error.message}`); // 在模態框內給出反饋
|
|
} finally {
|
|
if (this.elements.saveBtn) {
|
|
this.elements.saveBtn.disabled = false;
|
|
this.elements.saveBtn.textContent = 'Save Changes';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// --- 所有表單處理輔助方法 ---
|
|
_populateForm(data = {}) {
|
|
// [完整遷移] 填充表單的邏輯
|
|
const isStreamOptimizerEnabled = !!data.stream_optimizer_enabled;
|
|
this._setToggle(this.elements.streamOptimizerEnabled, isStreamOptimizerEnabled);
|
|
this._toggleStreamingPanel(isStreamOptimizerEnabled);
|
|
this._setValue(this.elements.streamMinDelay, data.stream_min_delay);
|
|
this._setValue(this.elements.streamMaxDelay, data.stream_max_delay);
|
|
this._setValue(this.elements.streamShortTextThresh, data.stream_short_text_threshold);
|
|
this._setValue(this.elements.streamLongTextThresh, data.stream_long_text_threshold);
|
|
this._setValue(this.elements.streamChunkSize, data.stream_chunk_size);
|
|
this._setToggle(this.elements.fakeStreamEnabled, data.fake_stream_enabled);
|
|
this._setValue(this.elements.fakeStreamInterval, data.fake_stream_empty_data_interval_seconds);
|
|
this._setToggle(this.elements.toolsCodeExecutionEnabled, data.tools_code_execution_enabled);
|
|
this._setToggle(this.elements.urlContextEnabled, data.url_context_enabled);
|
|
this._setToggle(this.elements.showSearchLink, data.show_search_link);
|
|
this._setToggle(this.elements.showThinkingProcess, data.show_thinking_process);
|
|
this._setValue(this.elements.configOverrides, data.config_overrides);
|
|
// --- Dynamic & Complex Fields ---
|
|
this._populateKVItems(this.elements.customHeadersContainer, data.custom_headers, this.addCustomHeaderItem.bind(this));
|
|
|
|
this._clearContainer(this.elements.safetySettingsContainer);
|
|
if (data.safety_settings && typeof data.safety_settings === 'object') {
|
|
for (const [key, value] of Object.entries(data.safety_settings)) {
|
|
this.addSafetySettingItem(key, value);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Collects all data from the form fields and returns it as an object.
|
|
* @returns {object} The collected request configuration data.
|
|
*/
|
|
collectFormData() {
|
|
return {
|
|
// Simple Toggles & Inputs
|
|
stream_optimizer_enabled: this.elements.streamOptimizerEnabled.checked,
|
|
stream_min_delay: parseInt(this.elements.streamMinDelay.value, 10),
|
|
stream_max_delay: parseInt(this.elements.streamMaxDelay.value, 10),
|
|
stream_short_text_threshold: parseInt(this.elements.streamShortTextThresh.value, 10),
|
|
stream_long_text_threshold: parseInt(this.elements.streamLongTextThresh.value, 10),
|
|
stream_chunk_size: parseInt(this.elements.streamChunkSize.value, 10),
|
|
fake_stream_enabled: this.elements.fakeStreamEnabled.checked,
|
|
fake_stream_empty_data_interval_seconds: parseInt(this.elements.fakeStreamInterval.value, 10),
|
|
tools_code_execution_enabled: this.elements.toolsCodeExecutionEnabled.checked,
|
|
url_context_enabled: this.elements.urlContextEnabled.checked,
|
|
show_search_link: this.elements.showSearchLink.checked,
|
|
show_thinking_process: this.elements.showThinkingProcess.checked,
|
|
config_overrides: this.elements.configOverrides.value,
|
|
|
|
// Dynamic & Complex Fields
|
|
custom_headers: this._collectKVItems(this.elements.customHeadersContainer),
|
|
safety_settings: this._collectSafetySettings(this.elements.safetySettingsContainer),
|
|
|
|
// TODO: Collect from Tag Inputs
|
|
// image_models: this.imageModelsInput.getValues(),
|
|
};
|
|
}
|
|
|
|
// 控制流式面板显示/隐藏的辅助函数
|
|
_toggleStreamingPanel(is_enabled) {
|
|
if (this.elements.streamingSettingsPanel) {
|
|
if (is_enabled) {
|
|
this.elements.streamingSettingsPanel.classList.remove('hidden');
|
|
} else {
|
|
this.elements.streamingSettingsPanel.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new key-value pair item for Custom Headers.
|
|
* @param {string} [key=''] - The initial key.
|
|
* @param {string} [value=''] - The initial value.
|
|
*/
|
|
addCustomHeaderItem(key = '', value = '') {
|
|
const container = this.elements.customHeadersContainer;
|
|
const item = document.createElement('div');
|
|
item.className = 'dynamic-kv-item';
|
|
item.innerHTML = `
|
|
<input type="text" class="modal-input text-xs bg-zinc-100 dark:bg-zinc-700/50" placeholder="Header Name" value="${key}">
|
|
<input type="text" class="modal-input text-xs" placeholder="Header Value" value="${value}">
|
|
<button type="button" class="remove-btn text-zinc-400 hover:text-red-500 transition-colors"><i class="fas fa-trash-alt"></i></button>
|
|
`;
|
|
container.appendChild(item);
|
|
}
|
|
|
|
/**
|
|
* Adds a new item for Safety Settings.
|
|
* @param {string} [category=''] - The initial category.
|
|
* @param {string} [threshold=''] - The initial threshold.
|
|
*/
|
|
addSafetySettingItem(category = '', threshold = '') {
|
|
const container = this.elements.safetySettingsContainer;
|
|
const item = document.createElement('div');
|
|
item.className = 'safety-setting-item flex items-center gap-x-2';
|
|
|
|
const harmCategories = [
|
|
"HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
|
|
"HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT","HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
"HARM_CATEGORY_CIVIC_INTEGRITY"
|
|
];
|
|
const harmThresholds = [
|
|
"BLOCK_OFF","BLOCK_NONE", "BLOCK_LOW_AND_ABOVE",
|
|
"BLOCK_MEDIUM_AND_ABOVE", "BLOCK_ONLY_HIGH"
|
|
];
|
|
|
|
const categorySelect = document.createElement('select');
|
|
categorySelect.className = 'modal-input flex-grow'; // .modal-input 在静态<select>上是有效的
|
|
harmCategories.forEach(cat => {
|
|
const option = new Option(cat.replace('HARM_CATEGORY_', ''), cat);
|
|
if (cat === category) option.selected = true;
|
|
categorySelect.add(option);
|
|
});
|
|
const thresholdSelect = document.createElement('select');
|
|
thresholdSelect.className = 'modal-input w-48';
|
|
harmThresholds.forEach(thr => {
|
|
const option = new Option(thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+'), thr);
|
|
if (thr === threshold) option.selected = true;
|
|
thresholdSelect.add(option);
|
|
});
|
|
|
|
const removeButton = document.createElement('button');
|
|
removeButton.type = 'button';
|
|
removeButton.className = 'remove-btn text-zinc-400 hover:text-red-500 transition-colors';
|
|
removeButton.innerHTML = `<i class="fas fa-trash-alt"></i>`;
|
|
item.appendChild(categorySelect);
|
|
item.appendChild(thresholdSelect);
|
|
item.appendChild(removeButton);
|
|
container.appendChild(item);
|
|
}
|
|
|
|
// --- Private Helper Methods for Form Handling ---
|
|
|
|
_setValue(element, value) {
|
|
if (element && value !== null && value !== undefined) {
|
|
element.value = value;
|
|
}
|
|
}
|
|
|
|
_setToggle(element, value) {
|
|
if (element) {
|
|
element.checked = !!value;
|
|
}
|
|
}
|
|
|
|
_clearContainer(container) {
|
|
if (container) {
|
|
// Keep the first child if it's a template or header
|
|
const firstChild = container.firstElementChild;
|
|
const isTemplate = firstChild && (firstChild.tagName === 'TEMPLATE' || firstChild.id === 'kv-item-header');
|
|
|
|
let child = isTemplate ? firstChild.nextElementSibling : container.firstElementChild;
|
|
while (child) {
|
|
const next = child.nextElementSibling;
|
|
child.remove();
|
|
child = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
_populateKVItems(container, items, addItemFn) {
|
|
this._clearContainer(container);
|
|
if (items && typeof items === 'object') {
|
|
for (const [key, value] of Object.entries(items)) {
|
|
addItemFn(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
_collectKVItems(container) {
|
|
const items = {};
|
|
container.querySelectorAll('.dynamic-kv-item').forEach(item => {
|
|
const keyEl = item.querySelector('.dynamic-kv-key');
|
|
const valueEl = item.querySelector('.dynamic-kv-value');
|
|
if (keyEl && valueEl && keyEl.value) {
|
|
items[keyEl.value] = valueEl.value;
|
|
}
|
|
});
|
|
return items;
|
|
}
|
|
|
|
_collectSafetySettings(container) {
|
|
const items = {};
|
|
container.querySelectorAll('.safety-setting-item').forEach(item => {
|
|
const categorySelect = item.querySelector('select:first-child');
|
|
const thresholdSelect = item.querySelector('select:last-of-type');
|
|
if (categorySelect && thresholdSelect && categorySelect.value) {
|
|
items[categorySelect.value] = thresholdSelect.value;
|
|
}
|
|
});
|
|
return items;
|
|
}
|
|
} |