Files
gemini-banlancer/frontend/js/pages/keys/requestSettingsModal.js
2025-11-20 12:24:05 +08:00

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;
}
}