mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-08 02:28:25 -05:00
- showQuizStats.js: add escapeHtml() and sanitize fileName/reason/details
before injecting into verificationModal.innerHTML (XSS: DOM text reinterpreted as HTML)
- injectQuizBtn.js: replace quizTitle string interpolation in innerHTML with
DOM construction (textContent) to prevent XSS (DOM text reinterpreted as HTML)
- highlight_menu.js: fix 'classList.contains === "hidden"' type error —
was comparing function reference to string; now correctly called as
classList.contains("hidden") (comparison between inconvertible types)
- index.html + indexHtml.js: rename malformed space-containing id attributes
'Show answers' -> 'show-answers' and 'Show chain of thought' -> 'show-chain-of-thought'
- settings.js: update three matching string keys to kebab-case to stay in sync
with renamed HTML ids (coordinated rename, no functionality change)
- demo_reference_rendering.html: add safeParseReferences() fallback wrapper,
replace direct parseReferences() call which was undefined in this context
- test_reference_renderer.js: remove parseReferences import (not exported),
rewrite testReferenceParsing() to use processReferences() with HTML output assertions
717 lines
26 KiB
JavaScript
717 lines
26 KiB
JavaScript
import { showPopover } from "../../libs/utils/utils";
|
|
import { DIFFICULTY_LEVELS } from "../../../configs/client.config.js";
|
|
import { DropdownHandler } from './dropdown-handler.js';
|
|
import { enableTooltip } from '../tooltip/tooltip.js';
|
|
|
|
|
|
export class SettingsManager {
|
|
constructor(shadowEle, isNoSlider=false) {
|
|
this.allShadow = shadowEle;
|
|
this.isNoSlider = isNoSlider;
|
|
this.isInitializing = true;
|
|
|
|
// Only initialize full settings if isNoSlider is false
|
|
if (!isNoSlider) {
|
|
this.shadowEle = shadowEle.querySelector("#modal1");
|
|
this.settings = {
|
|
sliderValue: 1,
|
|
selectedDropdownValue: "",
|
|
checkboxes: {},
|
|
modalDisplayed: false,
|
|
pushContent: false,
|
|
customAPI: {
|
|
provider: "",
|
|
endpoint: "",
|
|
model: "",
|
|
apiKey: "",
|
|
saveLocally: false,
|
|
enabled: false
|
|
}
|
|
};
|
|
this.loadSettings();
|
|
this.initializeMenuBehavior();
|
|
}
|
|
this.isInitializing = false;
|
|
}
|
|
|
|
// Initialize settings from the UI components
|
|
init() {
|
|
if (!this.isNoSlider) {
|
|
// Set up the slider
|
|
const slider = this.shadowEle.querySelector("#understanding-slider");
|
|
if (slider) {
|
|
slider.addEventListener("input", (event) => {
|
|
this.updateSlider(event.target.value);
|
|
});
|
|
this.updateSlider(slider.value);
|
|
}
|
|
|
|
// Set up checkboxes
|
|
const checkboxes = this.shadowEle.querySelectorAll(
|
|
'input[type="checkbox"]'
|
|
);
|
|
|
|
checkboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener("change", (event) => {
|
|
this.updateCheckbox(checkbox.id, event.target.checked);
|
|
});
|
|
this.updateCheckbox(checkbox.id, checkbox.checked);
|
|
});
|
|
|
|
// Initialize difficulty dropdowns
|
|
this.initializeDifficultyDropdown();
|
|
|
|
// Initialize difficulty dropdown toggle
|
|
this.initializeDifficultyDropdownToggle();
|
|
|
|
// Initialize custom API settings
|
|
this.initializeCustomAPISettings();
|
|
|
|
this.dispatchAndUpdate("init", true);
|
|
this.dispatchAndUpdate_settings("init", true);
|
|
} else {
|
|
// When isNoSlider is true, only initialize dropdowns
|
|
const difficultyDropdowns = this.allShadow.querySelectorAll('.difficulty-dropdown');
|
|
difficultyDropdowns.forEach(dropdown => {
|
|
this.initializeSingleDropdown(dropdown, true);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Dispatch highlight-menu event
|
|
dispatchHighlightMenuEvent(isEnabled) {
|
|
const event = new CustomEvent("highlight-menu-starts", {
|
|
detail: { enabled: isEnabled },
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
// Helper function to dispatch custom events and call alert
|
|
dispatchAndUpdate(key, value) {
|
|
// Dispatch custom event
|
|
const event = new CustomEvent("aiActionCompleted", {
|
|
detail: { type: "system_prompt", text: this.generateSystemPrompt() },
|
|
});
|
|
window.dispatchEvent(event);
|
|
// if (key !== "init")
|
|
// alert(this.shadowEle, "Option " + key + " with value " + value + " is updated", "success");
|
|
}
|
|
|
|
dispatchAndUpdate_settings(key, value) {
|
|
// Dispatch custom event
|
|
const event = new CustomEvent("aiActionCompleted", {
|
|
detail: {
|
|
settings: this.generateSettings(), // the original query object used for the request
|
|
type: "settings", // the type of AI action
|
|
},
|
|
});
|
|
window.dispatchEvent(event);
|
|
|
|
if (key !== "init") {
|
|
let displayValue = value;
|
|
if (typeof value === 'object' && value !== null) {
|
|
if (key === "customAPI" && value.provider) {
|
|
displayValue = `${value.provider} API`;
|
|
} else {
|
|
displayValue = JSON.stringify(value);
|
|
}
|
|
}
|
|
showPopover(this.shadowEle, "setting " + key + " with value " + displayValue + " is updated", "success");
|
|
}
|
|
}
|
|
|
|
// Update slider value in the settings
|
|
updateSlider(value) {
|
|
this.settings.sliderValue = parseInt(value, 10);
|
|
this.saveSettings();
|
|
|
|
window.current_difficulty = this.settings.sliderValue;
|
|
|
|
// Only dispatch if not initializing
|
|
if (!this.isInitializing) {
|
|
const event = new CustomEvent("aiActionCompleted", {
|
|
detail: {
|
|
type: "system_prompt",
|
|
text: DIFFICULTY_LEVELS[this.settings.sliderValue]
|
|
},
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
// Update all difficulty dropdowns
|
|
const difficultyLevels = ['🚲 Beginner', '🚗 Intermediate', '🚁 Advanced', '🛸 Bloom\'s Taxonomy'];
|
|
const currentLevelElements = this.allShadow.querySelectorAll('.current-difficulty-level');
|
|
currentLevelElements.forEach(element => {
|
|
element.textContent = difficultyLevels[value];
|
|
});
|
|
|
|
// Update the visual highlighting (existing code)
|
|
const difficultyLevelElements = this.shadowEle.querySelectorAll('.difficulty-level');
|
|
difficultyLevelElements.forEach((level, index) => {
|
|
if (index === this.settings.sliderValue) {
|
|
level.classList.add('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
} else {
|
|
level.classList.remove('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update dropdown value in the settings
|
|
updateDropdown(value) {
|
|
this.settings.selectedDropdownValue = value;
|
|
// this.saveSettings(); // Save settings to local storage
|
|
// this.dispatchAndUpdate_settings(
|
|
// "selectedDropdownValue",
|
|
// this.settings.selectedDropdownValue
|
|
// );
|
|
}
|
|
|
|
// Update checkbox status in the settings
|
|
updateCheckbox(id, isChecked) {
|
|
this.settings.checkboxes[id] = isChecked;
|
|
this.saveSettings(); // Save settings to local storage
|
|
|
|
if (id === "show-answers") {
|
|
this.dispatchAndUpdate(id, isChecked);
|
|
} else {
|
|
this.dispatchAndUpdate_settings(id, isChecked);
|
|
}
|
|
}
|
|
|
|
generateSystemPrompt() {
|
|
const understandingLevels = [
|
|
"Beginner: Focus on foundational concepts, definitions, and straightforward applications in machine learning systems, suitable for learners with little to no prior knowledge.",
|
|
"Intermediate: Emphasize problem-solving, system design, and practical implementations, targeting learners with a basic understanding of machine learning principles.",
|
|
"Advanced: Challenge learners to analyze, innovate, and optimize complex machine learning systems, requiring deep expertise and a holistic grasp of advanced techniques.",
|
|
"requesting Bloom's Taxonomy: You are an expert ML teacher using Bloom's Taxonomy: Create responses that progress through Bloom's levels: remember, understand, apply, analyze, evaluate, and create." //Bloom's Taxonomy: Bloom's Taxonomy is an educational framework ranking cognitive skills from basic recall to complex evaluation. https://en.wikipedia.org/wiki/Bloom%27s_taxonomy"
|
|
];
|
|
|
|
const understanding = understandingLevels[this.settings.sliderValue] || "unknown level";
|
|
|
|
const difficultyLevels = this.shadowEle.querySelectorAll('.difficulty-level');
|
|
difficultyLevels.forEach((level, index) => {
|
|
if (index === this.settings.sliderValue) {
|
|
level.classList.add('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
} else {
|
|
level.classList.remove('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
}
|
|
});
|
|
|
|
const showAnswers = this.settings.checkboxes["show-answers"];
|
|
const useBlooms = this.settings.checkboxes["Apply-blooms-taxonomy"];
|
|
|
|
const answersDescription = '';
|
|
showPopover(this.shadowEle, "Question difficulty level: " + understandingLevels[this.settings.sliderValue].split(':')[0], "success");
|
|
|
|
return `Tailor your response for a: ${understanding}. ${answersDescription} ${useBlooms ? 'Apply Bloom\'s Taxonomy in your response structure.' : ''}`;
|
|
}
|
|
|
|
generateSettings() {
|
|
// Prepare the dropdown value description
|
|
const llm_model = this.settings.selectedDropdownValue;
|
|
|
|
// Check the checkbox for showing answers
|
|
const show_progress = this.settings.checkboxes["show-chain-of-thought"];
|
|
|
|
return {
|
|
llm_model: llm_model,
|
|
show_progress: show_progress,
|
|
customAPI: this.settings.customAPI
|
|
};
|
|
}
|
|
|
|
// Save settings to local storage
|
|
saveSettings() {
|
|
localStorage.setItem("userSettings", JSON.stringify(this.settings));
|
|
}
|
|
|
|
// Load settings from local storage
|
|
loadSettings() {
|
|
const savedSettings = localStorage.getItem("userSettings");
|
|
if (savedSettings) {
|
|
this.settings = JSON.parse(savedSettings);
|
|
}
|
|
|
|
window.current_difficulty = this.settings.sliderValue;
|
|
|
|
const slider = this.shadowEle.querySelector("#understanding-slider");
|
|
if (slider) {
|
|
slider.value = this.settings.sliderValue;
|
|
// Remove the dispatch from here since updateSlider will handle it
|
|
|
|
// Update the visual highlighting for initial load
|
|
const difficultyLevels = this.shadowEle.querySelectorAll('.difficulty-level');
|
|
difficultyLevels.forEach((level, index) => {
|
|
if (index === this.settings.sliderValue) {
|
|
level.classList.add('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
} else {
|
|
level.classList.remove('bg-blue-50', 'dark:bg-zinc-700', 'p-2', 'rounded');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add these methods to your SettingsManager class
|
|
initializeDifficultyDropdown() {
|
|
const difficultyDropdowns = this.allShadow.querySelectorAll('.difficulty-dropdown');
|
|
difficultyDropdowns.forEach(dropdown => {
|
|
});
|
|
// const dropdownHandler = new DropdownHandler(this.allShadow);
|
|
|
|
difficultyDropdowns.forEach(dropdown => {
|
|
if (!dropdown.dataset.initialized) {
|
|
// Set initial difficulty level
|
|
const currentLevel = dropdown.querySelector('.current-difficulty-level');
|
|
const difficultyLevels = ['🚲 Beginner', '🚗 Intermediate', '🚁 Advanced', '🛸 Bloom\'s Taxonomy'];
|
|
if (currentLevel) {
|
|
// Remove emoji for display
|
|
currentLevel.textContent = difficultyLevels[this.settings.sliderValue].slice(2).trim();
|
|
}
|
|
|
|
// Add click handler for the dropdown button
|
|
const button = dropdown.querySelector('button');
|
|
const options = dropdown.querySelector('.difficulty-options');
|
|
|
|
if (button) {
|
|
button.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
options.classList.toggle('hidden'); // Toggle visibility
|
|
});
|
|
}
|
|
|
|
// Mark this dropdown as initialized
|
|
dropdown.dataset.initialized = true;
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', () => {
|
|
options.classList.add('hidden');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
initializeSingleDropdown(dropdown, isRedo=false) {
|
|
const dropdownHandler = new DropdownHandler(this.allShadow);
|
|
// enableTooltip(dropdown, "Select a difficulty level to redo the AI response", this.allShadow);
|
|
|
|
const currentLevel = dropdown.querySelector('.current-difficulty-level');
|
|
const difficultyLevels = ['🚲 Beginner', '🚗 Intermediate', '🚁 Advanced', '🛸 Bloom\'s Taxonomy'];
|
|
|
|
// Only set initial text if not in noSlider mode
|
|
if (currentLevel && !isRedo && !this.isNoSlider) {
|
|
currentLevel.textContent = difficultyLevels[this.settings.sliderValue].slice(2).trim();
|
|
}
|
|
|
|
const button = dropdown.querySelector('button');
|
|
enableTooltip(button, "Redo this AI response with this a new learner understanding level", this.allShadow);
|
|
const options = dropdown.querySelector('.difficulty-options');
|
|
|
|
if (button) {
|
|
button.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
options.classList.toggle('hidden'); // Toggle visibility
|
|
});
|
|
}
|
|
|
|
const optionElements = dropdown.querySelectorAll('.difficulty-option');
|
|
optionElements.forEach((option, index) => {
|
|
option.addEventListener('click', () => {
|
|
// this.updateSlider(index);
|
|
currentLevel.textContent = difficultyLevels[index].slice(2).trim();
|
|
options.classList.add('hidden'); // Hide options after selection
|
|
// Get the parent ai-message element
|
|
const aiMessageComponent = dropdown.closest('.ai-message-chat');
|
|
|
|
|
|
|
|
if (!aiMessageComponent) {
|
|
console.error('Could not find parent AI message component');
|
|
return;
|
|
}
|
|
|
|
|
|
// Create and inject progress element
|
|
const progressElement = document.createElement('div');
|
|
progressElement.id = 'progress';
|
|
|
|
// Find the content container within aiMessageComponent
|
|
const contentContainer = aiMessageComponent.querySelector('.markdown-preview-container');
|
|
|
|
const shareButtonContainer = aiMessageComponent.querySelector('.copy-paste-share-btn-container');
|
|
if (shareButtonContainer) {
|
|
shareButtonContainer.remove();
|
|
}
|
|
|
|
|
|
|
|
if (contentContainer) {
|
|
// Insert progress element before the content container
|
|
contentContainer.parentNode.insertBefore(progressElement, contentContainer);
|
|
|
|
// Find markdown preview container and add id
|
|
|
|
contentContainer.id = 'markdown-preview';
|
|
} else {
|
|
console.error('Could not find content container in AI message');
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
dropdownHandler.handleDifficultySelection(dropdown, index, aiMessageComponent);
|
|
});
|
|
});
|
|
|
|
dropdown.dataset.initialized = true;
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!dropdown.contains(e.target)) {
|
|
options.classList.add('hidden'); // Hide options when clicking outside
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add this method to handle the push content toggle
|
|
updatePushContent(isPushed) {
|
|
this.settings.pushContent = isPushed;
|
|
this.saveSettings();
|
|
|
|
// If menu is currently open, update the margin immediately
|
|
const menu = this.allShadow.querySelector('#text-selection-menu');
|
|
const isMenuOpen = menu && menu.classList.contains('translate-x-0');
|
|
|
|
if (isMenuOpen) {
|
|
document.body.style.marginRight = isPushed ? '400px' : '0';
|
|
document.body.style.transition = 'margin-right 0.3s ease-in-out';
|
|
}
|
|
}
|
|
|
|
// Add as a class method
|
|
initializeMenuBehavior() {
|
|
const body = document.querySelector('#mybody');
|
|
const menu = this.allShadow.querySelector('#text-selection-menu');
|
|
const toggle = this.shadowEle.querySelector('#push-content-toggle');
|
|
|
|
if (!toggle) {
|
|
console.warn('Push content toggle not found');
|
|
return;
|
|
}
|
|
|
|
// Load initial state from settings
|
|
const isPushEnabled = this.settings.pushContent || false;
|
|
|
|
// Set initial states
|
|
toggle.checked = isPushEnabled;
|
|
document.body.classList.toggle('push-content-enabled', isPushEnabled);
|
|
document.body.classList.toggle('push-content-disabled', !isPushEnabled);
|
|
|
|
// Handle toggle changes
|
|
toggle.addEventListener('change', (e) => {
|
|
const isPushed = e.target.checked;
|
|
this.updatePushContent(isPushed);
|
|
});
|
|
|
|
// Listen for menu display mode changes
|
|
window.addEventListener('menuDisplayModeChanged', (e) => {
|
|
const isPushed = e.detail.pushContent;
|
|
document.body.classList.toggle('push-content-enabled', isPushed);
|
|
document.body.classList.toggle('push-content-disabled', !isPushed);
|
|
});
|
|
|
|
// Update classes when menu opens/closes
|
|
const menuToggle = this.allShadow.querySelector('#menu-toggle');
|
|
if (menuToggle) {
|
|
menuToggle.addEventListener('change', (e) => {
|
|
document.body.classList.toggle('menu-open', e.target.checked);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize difficulty dropdown toggle
|
|
initializeDifficultyDropdownToggle() {
|
|
const dropdownToggle = this.shadowEle.querySelector('#difficulty-dropdown-toggle');
|
|
const difficultyContent = this.shadowEle.querySelector('#difficulty-content');
|
|
|
|
if (!dropdownToggle || !difficultyContent) {
|
|
console.warn('Difficulty dropdown elements not found');
|
|
return;
|
|
}
|
|
|
|
// Set initial state - collapsed by default
|
|
difficultyContent.classList.add('difficulty-content-collapsed');
|
|
difficultyContent.classList.remove('difficulty-content-expanded');
|
|
|
|
// Handle toggle click
|
|
dropdownToggle.addEventListener('click', () => {
|
|
const isCollapsed = difficultyContent.classList.contains('difficulty-content-collapsed');
|
|
|
|
if (isCollapsed) {
|
|
// Expand
|
|
difficultyContent.classList.remove('difficulty-content-collapsed');
|
|
difficultyContent.classList.add('difficulty-content-expanded');
|
|
dropdownToggle.classList.add('rotated');
|
|
} else {
|
|
// Collapse
|
|
difficultyContent.classList.remove('difficulty-content-expanded');
|
|
difficultyContent.classList.add('difficulty-content-collapsed');
|
|
dropdownToggle.classList.remove('rotated');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize custom API settings
|
|
initializeCustomAPISettings() {
|
|
const customApiToggle = this.shadowEle.querySelector("#custom-api-toggle");
|
|
const customApiConfig = this.shadowEle.querySelector("#custom-api-config");
|
|
const defaultApiInfo = this.shadowEle.querySelector("#default-api-info");
|
|
const providerSelect = this.shadowEle.querySelector("#ai-provider");
|
|
const modelInput = this.shadowEle.querySelector("#api-model");
|
|
const endpointInput = this.shadowEle.querySelector("#api-endpoint");
|
|
const apiKeyInput = this.shadowEle.querySelector("#api-key");
|
|
const saveLocallyCheckbox = this.shadowEle.querySelector("#save-locally");
|
|
const resetButton = this.shadowEle.querySelector("#reset-custom-api");
|
|
|
|
if (!customApiToggle || !customApiConfig || !defaultApiInfo || !providerSelect || !modelInput || !endpointInput || !apiKeyInput || !saveLocallyCheckbox || !resetButton) {
|
|
console.warn('Custom API elements not found in settings modal');
|
|
return;
|
|
}
|
|
|
|
// Load existing values
|
|
if (this.settings.customAPI) {
|
|
customApiToggle.checked = this.settings.customAPI.enabled || false;
|
|
providerSelect.value = this.settings.customAPI.provider || "";
|
|
modelInput.value = this.settings.customAPI.model || "";
|
|
endpointInput.value = this.settings.customAPI.endpoint || "";
|
|
apiKeyInput.value = this.settings.customAPI.apiKey || "";
|
|
saveLocallyCheckbox.checked = this.settings.customAPI.saveLocally || false;
|
|
}
|
|
|
|
// Update UI based on toggle state
|
|
this.updateCustomAPIUI(customApiToggle.checked, customApiConfig, defaultApiInfo);
|
|
|
|
// Add change listeners
|
|
customApiToggle.addEventListener("change", (e) => {
|
|
this.updateCustomAPIUI(e.target.checked, customApiConfig, defaultApiInfo);
|
|
this.updateCustomAPI(providerSelect.value, endpointInput.value, modelInput.value, apiKeyInput.value, saveLocallyCheckbox.checked, e.target.checked);
|
|
});
|
|
|
|
providerSelect.addEventListener("change", (e) => {
|
|
this.handleProviderChange(e.target.value, modelInput, endpointInput);
|
|
this.updateCustomAPI(e.target.value, endpointInput.value, modelInput.value, apiKeyInput.value, saveLocallyCheckbox.checked, customApiToggle.checked);
|
|
});
|
|
|
|
modelInput.addEventListener("input", (e) => {
|
|
this.updateCustomAPI(providerSelect.value, endpointInput.value, e.target.value, apiKeyInput.value, saveLocallyCheckbox.checked, customApiToggle.checked);
|
|
});
|
|
|
|
endpointInput.addEventListener("input", (e) => {
|
|
this.updateCustomAPI(providerSelect.value, e.target.value, modelInput.value, apiKeyInput.value, saveLocallyCheckbox.checked, customApiToggle.checked);
|
|
});
|
|
|
|
apiKeyInput.addEventListener("input", (e) => {
|
|
this.updateCustomAPI(providerSelect.value, endpointInput.value, modelInput.value, e.target.value, saveLocallyCheckbox.checked, customApiToggle.checked);
|
|
});
|
|
|
|
saveLocallyCheckbox.addEventListener("change", (e) => {
|
|
this.updateCustomAPI(providerSelect.value, endpointInput.value, modelInput.value, apiKeyInput.value, e.target.checked, customApiToggle.checked);
|
|
});
|
|
|
|
resetButton.addEventListener("click", (e) => {
|
|
this.resetCustomAPISettings(providerSelect, modelInput, endpointInput, apiKeyInput, saveLocallyCheckbox, customApiToggle);
|
|
});
|
|
}
|
|
|
|
// Handle provider change and auto-fill endpoint and model
|
|
handleProviderChange(provider, modelInput, endpointInput) {
|
|
const providerDefaults = {
|
|
'google-gemini': {
|
|
endpoint: 'https://generativelanguage.googleapis.com/v1beta',
|
|
model: 'gemini-2.5-flash'
|
|
},
|
|
'ollama': {
|
|
endpoint: 'http://localhost:11434/api/generate',
|
|
model: 'gemma3:270m'
|
|
},
|
|
'openai': {
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
model: 'gpt-3.5-turbo'
|
|
},
|
|
'open-router': {
|
|
endpoint: 'https://openrouter.ai/api/v1/chat/completions',
|
|
model: 'meta-llama/llama-4-scout:free'
|
|
},
|
|
'groq': {
|
|
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
|
|
model: 'llama-3.1-8b-instant'
|
|
},
|
|
'anthropic': {
|
|
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
model: 'claude-3-sonnet-20240229'
|
|
}
|
|
};
|
|
|
|
if (provider && providerDefaults[provider]) {
|
|
const defaults = providerDefaults[provider];
|
|
|
|
// Always auto-fill when provider changes
|
|
endpointInput.value = defaults.endpoint;
|
|
modelInput.value = defaults.model;
|
|
|
|
console.log(`[AUTO_FILL] Provider changed to ${provider}:`, defaults);
|
|
} else if (provider === '') {
|
|
// Clear fields when no provider is selected
|
|
endpointInput.value = '';
|
|
modelInput.value = '';
|
|
}
|
|
}
|
|
|
|
// Update custom API UI based on toggle state
|
|
updateCustomAPIUI(enabled, customApiConfig, defaultApiInfo) {
|
|
if (enabled) {
|
|
customApiConfig.classList.remove('hidden');
|
|
defaultApiInfo.classList.add('hidden');
|
|
} else {
|
|
customApiConfig.classList.add('hidden');
|
|
defaultApiInfo.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Reset custom API settings to defaults
|
|
resetCustomAPISettings(providerSelect, modelInput, endpointInput, apiKeyInput, saveLocallyCheckbox, customApiToggle) {
|
|
// Reset all fields to empty/default values
|
|
providerSelect.value = "";
|
|
modelInput.value = "";
|
|
endpointInput.value = "";
|
|
apiKeyInput.value = "";
|
|
saveLocallyCheckbox.checked = false;
|
|
customApiToggle.checked = false;
|
|
|
|
// Update settings
|
|
this.settings.customAPI = {
|
|
provider: "",
|
|
endpoint: "",
|
|
model: "",
|
|
apiKey: "",
|
|
saveLocally: false,
|
|
enabled: false
|
|
};
|
|
|
|
this.saveSettings();
|
|
this.dispatchAndUpdate_settings("customAPI", this.settings.customAPI);
|
|
|
|
// Update UI
|
|
this.updateCustomAPIUI(false, this.shadowEle.querySelector("#custom-api-config"), this.shadowEle.querySelector("#default-api-info"));
|
|
|
|
showPopover(this.shadowEle, "Custom API settings reset to defaults", "success");
|
|
}
|
|
|
|
// Update custom API configuration
|
|
updateCustomAPI(provider, endpoint, model, apiKey, saveLocally, enabled) {
|
|
this.settings.customAPI = {
|
|
provider: provider || "",
|
|
endpoint: endpoint || "",
|
|
model: model || "",
|
|
apiKey: apiKey || "",
|
|
saveLocally: saveLocally || false,
|
|
enabled: enabled || false
|
|
};
|
|
|
|
this.saveSettings();
|
|
this.dispatchAndUpdate_settings("customAPI", this.settings.customAPI);
|
|
|
|
// Show success message
|
|
if (enabled && provider && endpoint) {
|
|
showPopover(this.shadowEle, `Custom API configured: ${provider}`, "success");
|
|
} else if (!enabled) {
|
|
showPopover(this.shadowEle, "Using SocratiQ default AI providers", "success");
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setupModal(shadowEle) {
|
|
const modal = shadowEle.querySelector("#modal1");
|
|
const settingsBtn = shadowEle.querySelector("#settings-btn");
|
|
enableTooltip(settingsBtn, "Open settings", shadowEle);
|
|
const closeBtn = modal.querySelector("#close-btn");
|
|
|
|
// Create overlay inside the shadow DOM instead of document body
|
|
const overlay = document.createElement("div");
|
|
overlay.classList.add("settings-modal-overlay");
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 99999; /* Increased z-index to be higher than the menu */
|
|
display: none;
|
|
pointer-events: auto; /* Ensure clicks are captured */
|
|
`;
|
|
|
|
// Style the modal to appear above the overlay
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 100000; /* Higher than overlay */
|
|
display: none;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
pointer-events: auto;
|
|
`;
|
|
|
|
// Append overlay to shadow root instead of document body
|
|
shadowEle.appendChild(overlay);
|
|
|
|
const toggleModal = () => {
|
|
const isDisplayed = modal.style.display === "block";
|
|
modal.style.display = isDisplayed ? "none" : "block";
|
|
overlay.style.display = isDisplayed ? "none" : "block";
|
|
|
|
if (!isDisplayed) {
|
|
document.body.style.overflow = "hidden";
|
|
// Ensure the overlay is above all shadow DOM content
|
|
overlay.style.position = "fixed";
|
|
overlay.style.zIndex = "99999";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
};
|
|
|
|
const closeModal = () => {
|
|
modal.style.display = "none";
|
|
overlay.style.display = "none";
|
|
document.body.style.overflow = ""; // Restore body scrolling
|
|
};
|
|
|
|
// Event listeners
|
|
settingsBtn.addEventListener("click", toggleModal);
|
|
closeBtn.addEventListener("click", closeModal);
|
|
overlay.addEventListener("click", closeModal);
|
|
|
|
// Prevent modal from closing when clicking inside it
|
|
modal.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
|
|
// Close modal with Escape key
|
|
window.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape" && modal.style.display === "block") {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// Clean up function to remove overlay when needed
|
|
return () => {
|
|
if (overlay && overlay.parentNode) {
|
|
overlay.parentNode.removeChild(overlay);
|
|
}
|
|
};
|
|
}
|