Created
April 1, 2026 04:57
-
-
Save pfn/34a9c9cf7b2f9861c3e0afdac80a2814 to your computer and use it in GitHub Desktop.
OpenWebUI loaded models extension for VLLM & llama.cpp server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Loaded Models Extension for Open WebUI | |
| * | |
| * This extension adds a "Loaded Models" section to the sidebar that displays | |
| * OpenAI-type models with status.value === 'loaded'. Users can click on these | |
| * models to immediately start a new chat with that model selected. | |
| * | |
| * Features: | |
| * - Fetches models from /api/models endpoint | |
| * - Filters for owned_by === 'openai' AND status?.value === 'loaded' | |
| * - Injects section after Workspace button (or fallback positions) | |
| * - Continuous polling every 30s to keep model data fresh | |
| * - Caches models in memory for instant sidebar population | |
| * - Watches for sidebar DOM element creation for immediate detection | |
| * | |
| * Dependencies: None (vanilla JavaScript) | |
| * Required Access: localStorage.token for API authentication | |
| */ | |
| // === CONFIG === | |
| const CONFIG = { | |
| POLLING_INTERVAL: 30000, // 30 seconds | |
| SECTION_ID: 'sidebar-loaded-models', | |
| LOADING_ID: 'loaded-models-loading' | |
| }; | |
| // === CACHE === | |
| // Hybrid cache: raw data array + pre-rendered DOM elements | |
| let loadedModelsCache = []; | |
| let domCache = new Map(); | |
| // === STATE === | |
| let state = { | |
| sidebarObserver: null, // Watches for sidebar DOM changes | |
| stateObserver: null, // Watches for data-state changes | |
| pollTimeoutId: null, | |
| isPolling: false | |
| }; | |
| // === DATA LAYER === | |
| /** | |
| * Fetch loaded models from the Open WebUI API | |
| * @returns {Promise<Array>} Array of loaded OpenAI models | |
| */ | |
| async function fetchLoadedModels() { | |
| try { | |
| const token = localStorage.token; | |
| if (!token) { | |
| console.warn('[Loaded Models Extension] No authentication token found'); | |
| return []; | |
| } | |
| const response = await fetch(`${window.WEBUI_BASE_URL || ''}/api/models`, { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'application/json', | |
| 'Content-Type': 'application/json', | |
| ...(token && { 'authorization': `Bearer ${token}` }) | |
| } | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json().catch(() => ({})); | |
| console.error('[Loaded Models Extension] Failed to fetch models:', error); | |
| return []; | |
| } | |
| const data = await response.json(); | |
| const allModels = data.data || []; | |
| // Filter for OpenAI models with status.value === 'loaded' | |
| const loadedModels = allModels.filter(model => { | |
| // model missing status implies it is loaded, e.g. in vllm | |
| // models that are presets will automatically appear in models tab, those are | |
| // aliases around a base model, we can't tell if they're loaded or not | |
| // well, we could tell, but that would involve diving down the rabbit hole of base models | |
| return model.owned_by === 'openai' && !model.preset && (( | |
| model.status && | |
| model.status.value === 'loaded') || | |
| (!model.status && (!model.info || !model.info.meta || !model.info.meta.hidden))); | |
| }); | |
| console.log(`[Loaded Models Extension] Found ${loadedModels.length} loaded models`, loadedModels); | |
| return loadedModels; | |
| } catch (error) { | |
| console.error('[Loaded Models Extension] Error fetching models:', error); | |
| return []; | |
| } | |
| } | |
| /** | |
| * Check if the model ID set has changed | |
| * @param {Set} newIds - New set of model IDs | |
| * @param {Set} oldIds - Old set of model IDs | |
| * @returns {boolean} True if IDs differ | |
| */ | |
| function hasCacheChanged(newIds, oldIds) { | |
| if (newIds.size !== oldIds.size) return true; | |
| for (const id of newIds) { | |
| if (!oldIds.has(id)) return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Update the model cache with new data | |
| * Rebuilds DOM cache only when model IDs change | |
| * @param {Array} newModels - New array of model objects | |
| * @returns {boolean} True if cache was updated | |
| */ | |
| function updateCache(newModels) { | |
| const newIds = new Set(newModels.map(m => m.id)); | |
| const oldIds = new Set(loadedModelsCache.map(m => m.id)); | |
| if (!hasCacheChanged(newIds, oldIds)) { | |
| return false; | |
| } | |
| // Only remove old DOM nodes if sidebar is NOT open | |
| // If sidebar is open, keep the nodes in place to avoid visual flicker | |
| const sidebarIsOpen = isSidebarOpen(); | |
| if (!sidebarIsOpen) { | |
| // Safely remove old DOM nodes before clearing cache | |
| domCache.forEach(el => { | |
| if (el.parentNode) { | |
| el.parentNode.removeChild(el); | |
| } | |
| }); | |
| } | |
| domCache.clear(); | |
| // Build new DOM cache | |
| newModels.forEach(model => { | |
| domCache.set(model.id, createModelItem(model)); | |
| }); | |
| loadedModelsCache = newModels; | |
| return true; | |
| } | |
| // === DOM LAYER === | |
| /** | |
| * Create a model item element matching PinnedModelItem.svelte styling | |
| * Uses Tailwind classes from the original Open WebUI component | |
| * | |
| * @param {Object} model - The model object to render | |
| * @returns {HTMLAnchorElement} Clickable model item element | |
| */ | |
| function createModelItem(model) { | |
| const WEBUI_BASE_URL = window.WEBUI_BASE_URL || ''; | |
| // Create the anchor element with exact Tailwind classes from PinnedModelItem.svelte | |
| const link = document.createElement('a'); | |
| link.className = 'grow flex items-center space-x-2.5 rounded-xl px-2.5 py-[7px] group-hover:bg-gray-100 dark:group-hover:bg-gray-900 transition'; | |
| link.href = `/?model=${encodeURIComponent(model.id)}`; | |
| link.draggable = 'false'; | |
| // Create container for the model icon | |
| const iconContainer = document.createElement('div'); | |
| iconContainer.className = 'self-center shrink-0'; | |
| // Create the model image element | |
| const img = document.createElement('img'); | |
| img.src = `${WEBUI_BASE_URL}/api/v1/models/model/profile/image?id=${encodeURIComponent(model.id)}&lang=en-US`; | |
| img.className = 'size-5 rounded-full -translate-x-[0.5px]'; | |
| img.alt = 'model icon'; | |
| img.draggable = 'false'; | |
| // Create container for the model name | |
| const nameContainer = document.createElement('div'); | |
| nameContainer.className = 'flex self-center translate-y-[0.5px]'; | |
| // Create the model name text element | |
| const nameText = document.createElement('div'); | |
| nameText.className = 'self-center text-sm font-primary line-clamp-1'; | |
| nameText.textContent = model.name || model.id; | |
| // Assemble the elements | |
| nameContainer.appendChild(nameText); | |
| iconContainer.appendChild(img); | |
| link.appendChild(iconContainer); | |
| link.appendChild(nameContainer); | |
| return link; | |
| } | |
| /** | |
| * Create the "Loaded Models" section container element | |
| * Matches Folder component styling from Sidebar.svelte | |
| * | |
| * @returns {Object} Object with section element and container div | |
| */ | |
| function createLoadedModelsSection() { | |
| // Container div (matches Folder's inner structure) | |
| const container = document.createElement('div'); | |
| container.className = 'px-2 py-2 mt-0.5 pb-1.5'; | |
| container.id = `${CONFIG.SECTION_ID}-container`; | |
| // Loading indicator (matches PinnedModelList loading style) | |
| const loadingEl = document.createElement('div'); | |
| loadingEl.className = 'px-2 w-full flex py-2 text-xs items-center gap-2'; | |
| loadingEl.id = CONFIG.LOADING_ID; | |
| loadingEl.textContent = "Loaded Models"; | |
| container.appendChild(loadingEl); | |
| // Section wrapper (matches Folder component structure) | |
| const section = document.createElement('div'); | |
| section.className = 'flex flex-col space-y-1 rounded-xl'; | |
| section.id = CONFIG.SECTION_ID; | |
| return { section, container }; | |
| } | |
| /** | |
| * Render the loaded models from cache into the sidebar | |
| * Only called when sidebar is confirmed open and cache has data | |
| */ | |
| function renderFromCache() { | |
| if (!isSidebarOpen()) { | |
| return; | |
| } | |
| // Don't render if no data | |
| if (loadedModelsCache.length === 0) { | |
| return; | |
| } | |
| // Remove existing section | |
| const existingSection = document.getElementById(CONFIG.SECTION_ID); | |
| if (existingSection) { | |
| existingSection.remove(); | |
| } | |
| const { section, container } = createLoadedModelsSection(); | |
| // Append cached DOM elements | |
| domCache.forEach(el => container.appendChild(el)); | |
| // Add section header | |
| const headerDiv = document.createElement('div'); | |
| headerDiv.className = 'mb-1'; | |
| headerDiv.appendChild(container); | |
| section.appendChild(headerDiv); | |
| // Find injection point using fallback strategy | |
| const targetElement = findInjectionPoint(); | |
| if (targetElement) { | |
| // Insert after the target element - find its parent wrapper | |
| let parentWrapper = targetElement.parentNode; | |
| // Try to find a reasonable parent container for injection | |
| if (parentWrapper && parentWrapper.tagName !== 'NAV') { | |
| parentWrapper.parentNode.insertBefore(section, parentWrapper.nextSibling); | |
| } else { | |
| // Fallback: append directly to sidebar content | |
| const navContent = document.querySelector('nav > div > div.overflow-y-auto'); | |
| if (navContent) { | |
| navContent.appendChild(section); | |
| } | |
| } | |
| console.log('[Loaded Models Extension] Section injected successfully'); | |
| } else { | |
| // Try to find the main sidebar content container and append there | |
| const navContent = document.querySelector('nav > div > div.overflow-y-auto'); | |
| if (navContent) { | |
| navContent.appendChild(section); | |
| console.log('[Loaded Models Extension] Section appended to sidebar as fallback'); | |
| } else { | |
| console.warn('[Loaded Models Extension] Could not find injection point for Loaded Models section'); | |
| } | |
| } | |
| } | |
| /** | |
| * Find the optimal injection point in the sidebar using fallback strategy | |
| * Priority order: Workspace → Models folder → Search button | |
| * | |
| * @returns {HTMLElement|null} Target element to inject after, or null | |
| */ | |
| function findInjectionPoint() { | |
| // Priority 1: After Workspace button (preferred) | |
| const workspaceButton = document.getElementById('sidebar-workspace-button'); | |
| if (workspaceButton && document.contains(workspaceButton)) { | |
| console.log('[Loaded Models Extension] Found Workspace button as injection point'); | |
| return workspaceButton; | |
| } | |
| // Priority 2: Before Models folder (if Workspace doesn't exist) | |
| const modelsFolder = document.getElementById('sidebar-models'); | |
| if (modelsFolder && document.contains(modelsFolder)) { | |
| console.log('[Loaded Models Extension] Found Models folder as injection point'); | |
| return modelsFolder; | |
| } | |
| // Priority 3: After Search button (guaranteed to exist) | |
| const searchButton = document.getElementById('sidebar-search-button'); | |
| if (searchButton && document.contains(searchButton)) { | |
| console.log('[Loaded Models Extension] Found Search button as fallback injection point'); | |
| return searchButton; | |
| } | |
| // Priority 4: After New Chat button (last resort) | |
| const newChatButton = document.getElementById('sidebar-new-chat-button'); | |
| if (newChatButton && document.contains(newChatButton)) { | |
| console.log('[Loaded Models Extension] Found New Chat button as last resort injection point'); | |
| return newChatButton; | |
| } | |
| console.warn('[Loaded Models Extension] No suitable injection point found'); | |
| return null; | |
| } | |
| /** | |
| * Check if the sidebar is currently open/visible by checking Svelte's data-state attribute | |
| * @returns {boolean} True if sidebar is visible | |
| */ | |
| function isSidebarOpen() { | |
| // Primary check: Look for the nav element with data-state="true" | |
| const navElement = document.querySelector('nav[data-state="true"]'); | |
| if (navElement) { | |
| return true; | |
| } | |
| // Secondary check: Look for sidebar div that is visible in DOM | |
| const sidebarElement = document.getElementById('sidebar'); | |
| if (sidebarElement && document.contains(sidebarElement)) { | |
| // Check data-state attribute first | |
| const dataState = sidebarElement.getAttribute('data-state'); | |
| if (dataState === 'true') { | |
| return true; | |
| } | |
| // Check if it has visibility classes that indicate it's shown | |
| const classList = sidebarElement.className || ''; | |
| if (classList.includes('visible') && !classList.includes('hidden')) { | |
| return true; | |
| } | |
| // Fallback: Check computed styles and actual rendering | |
| const style = window.getComputedStyle(sidebarElement); | |
| if (style.display !== 'none' && style.visibility !== 'hidden') { | |
| const rect = sidebarElement.getBoundingClientRect(); | |
| if (rect.width > 100 && rect.height > 100) { // Minimum reasonable size | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // === POLLING LAYER === | |
| /** | |
| * Run a single poll cycle | |
| * Fetches models and updates cache | |
| */ | |
| async function runPoll() { | |
| if (state.isPolling) { | |
| console.log('[Loaded Models Extension] Already polling, skipping'); | |
| return; | |
| } | |
| state.isPolling = true; | |
| try { | |
| const models = await fetchLoadedModels(); | |
| const cacheChanged = updateCache(models); | |
| // If sidebar is open and cache changed, re-render to update the items | |
| if (cacheChanged && isSidebarOpen()) { | |
| console.log('[Loaded Models Extension] Cache changed, sidebar open, re-rendering'); | |
| renderFromCache(); | |
| } else if (isSidebarOpen() && loadedModelsCache.length > 0) { | |
| // Fallback: ensure section exists if sidebar is open | |
| const existingSection = document.getElementById(CONFIG.SECTION_ID); | |
| if (!existingSection) { | |
| console.log('[Loaded Models Extension] Sidebar open but section missing, rendering'); | |
| renderFromCache(); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('[Loaded Models Extension] Poll error:', error); | |
| // Keep existing data intact on error | |
| } finally { | |
| state.isPolling = false; | |
| } | |
| // Schedule next poll 30s after this one completes (sequential polling) | |
| scheduleNextPoll(CONFIG.POLLING_INTERVAL); | |
| } | |
| /** | |
| * Schedule the next poll cycle | |
| * @param {number} delayMs - Delay in milliseconds | |
| */ | |
| function scheduleNextPoll(delayMs) { | |
| if (state.pollTimeoutId) { | |
| clearTimeout(state.pollTimeoutId); | |
| } | |
| state.pollTimeoutId = setTimeout(() => { | |
| runPoll(); | |
| }, delayMs); | |
| } | |
| /** | |
| * Start the polling cycle | |
| * Begins with immediate fetch, then schedules subsequent polls | |
| */ | |
| function startPolling() { | |
| // Clear any existing timeout | |
| if (state.pollTimeoutId) { | |
| clearTimeout(state.pollTimeoutId); | |
| } | |
| // Immediate first poll, then schedule subsequent ones | |
| runPoll(); | |
| } | |
| // === SIDEBAR OBSERVER LAYER === | |
| /** | |
| * Setup observer to watch for sidebar element being added/removed from DOM | |
| * Attached to div.app for immediate detection of sidebar creation | |
| */ | |
| function setupSidebarElementObserver() { | |
| // Clean up existing observer | |
| if (state.sidebarObserver) { | |
| state.sidebarObserver.disconnect(); | |
| state.sidebarObserver = null; | |
| } | |
| // Find the app container | |
| const appContainer = document.querySelector('div.app') || document.body; | |
| console.log('[Loaded Models Extension] Setting up sidebar element observer on', appContainer.tagName); | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.type === 'childList') { | |
| // Check if sidebar was added | |
| if (mutation.addedNodes.length > 0) { | |
| for (let node of mutation.addedNodes) { | |
| if (node.nodeType === Node.ELEMENT_NODE) { | |
| // Check if this is the sidebar or contains it | |
| const sidebar = node.id === 'sidebar' ? node : node.querySelector('#sidebar'); | |
| if (sidebar) { | |
| console.log('[Loaded Models Extension] Sidebar element detected in DOM'); | |
| // Check if it's already open | |
| if (isSidebarOpen()) { | |
| console.log('[Loaded Models Extension] Sidebar is open, rendering from cache'); | |
| renderFromCache(); | |
| } | |
| // Set up state observer on the nav/sidebar | |
| setupSidebarStateObserver(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // Check if sidebar was removed | |
| if (mutation.removedNodes.length > 0) { | |
| for (let node of mutation.removedNodes) { | |
| if (node.nodeType === Node.ELEMENT_NODE) { | |
| const sidebar = node.id === 'sidebar' ? node : node.querySelector('#sidebar'); | |
| if (sidebar) { | |
| console.log('[Loaded Models Extension] Sidebar element removed from DOM'); | |
| // Clean up state observer | |
| if (state.stateObserver) { | |
| state.stateObserver.disconnect(); | |
| state.stateObserver = null; | |
| } | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| // Observe for child elements being added/removed | |
| observer.observe(appContainer, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| state.sidebarObserver = observer; | |
| console.log('[Loaded Models Extension] Sidebar element observer attached'); | |
| } | |
| /** | |
| * Setup observer to watch for sidebar open/close state changes | |
| * Attached to nav and #sidebar elements | |
| */ | |
| function setupSidebarStateObserver() { | |
| // Clean up existing observer | |
| if (state.stateObserver) { | |
| state.stateObserver.disconnect(); | |
| state.stateObserver = null; | |
| } | |
| const navElement = document.querySelector('nav'); | |
| const sidebarElement = document.getElementById('sidebar'); | |
| if (!navElement && !sidebarElement) { | |
| console.log('[Loaded Models Extension] No nav/sidebar found for state observer'); | |
| return; | |
| } | |
| console.log('[Loaded Models Extension] Setting up sidebar state observer'); | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.type === 'attributes' && | |
| mutation.attributeName === 'data-state') { | |
| const nowOpen = isSidebarOpen(); | |
| if (nowOpen) { | |
| console.log('[Loaded Models Extension] Sidebar state changed to OPEN, rendering from cache'); | |
| renderFromCache(); | |
| } else { | |
| console.log('[Loaded Models Extension] Sidebar state changed to CLOSED'); | |
| } | |
| } | |
| }); | |
| }); | |
| // Observe nav element for state changes | |
| if (navElement) { | |
| observer.observe(navElement, { | |
| attributes: true, | |
| attributeFilter: ['data-state'] | |
| }); | |
| console.log('[Loaded Models Extension] State observer attached to nav'); | |
| } | |
| // Observe sidebar element for state changes | |
| if (sidebarElement && sidebarElement !== navElement) { | |
| observer.observe(sidebarElement, { | |
| attributes: true, | |
| attributeFilter: ['data-state'] | |
| }); | |
| console.log('[Loaded Models Extension] State observer attached to #sidebar'); | |
| } | |
| state.stateObserver = observer; | |
| } | |
| /** | |
| * Initialize sidebar observers - both element detection and state changes | |
| */ | |
| function initializeSidebarObservers() { | |
| // First, check if sidebar already exists | |
| const existingSidebar = document.getElementById('sidebar'); | |
| if (existingSidebar) { | |
| console.log('[Loaded Models Extension] Sidebar already exists on init'); | |
| if (isSidebarOpen()) { | |
| console.log('[Loaded Models Extension] Sidebar is open, rendering from cache'); | |
| renderFromCache(); | |
| } | |
| setupSidebarStateObserver(); | |
| } | |
| // Set up observer for when sidebar is added | |
| setupSidebarElementObserver(); | |
| } | |
| // === LIFECYCLE === | |
| /** | |
| * Initialize the Loaded Models Extension | |
| * Main entry point called by loader.js | |
| */ | |
| function initLoadedModelsExtension() { | |
| console.log('[Loaded Models Extension] Initializing...'); | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', onDomReady); | |
| } else { | |
| onDomReady(); | |
| } | |
| // Setup cleanup on page unload | |
| window.addEventListener('beforeunload', cleanup); | |
| console.log('[Loaded Models Extension] Initialization complete'); | |
| } | |
| /** | |
| * Handle DOM ready event - main initialization logic | |
| */ | |
| function onDomReady() { | |
| console.log('[Loaded Models Extension] DOM is ready, starting extension'); | |
| // Initialize sidebar observers (watches for sidebar creation and state changes) | |
| initializeSidebarObservers(); | |
| // Start polling for model updates | |
| startPolling(); | |
| } | |
| /** | |
| * Clean up extension resources (call on page unload or reinitialization) | |
| */ | |
| function cleanup() { | |
| if (state.pollTimeoutId) { | |
| clearTimeout(state.pollTimeoutId); | |
| } | |
| if (state.sidebarObserver) { | |
| state.sidebarObserver.disconnect(); | |
| state.sidebarObserver = null; | |
| } | |
| if (state.stateObserver) { | |
| state.stateObserver.disconnect(); | |
| state.stateObserver = null; | |
| } | |
| // Clear caches | |
| loadedModelsCache = []; | |
| domCache.clear(); | |
| // Remove the section from DOM | |
| const existingSection = document.getElementById(CONFIG.SECTION_ID); | |
| if (existingSection) { | |
| existingSection.remove(); | |
| } | |
| console.log('[Loaded Models Extension] Cleanup completed'); | |
| } | |
| // Export initialization function for loader.js | |
| export { initLoadedModelsExtension }; | |
| export default initLoadedModelsExtension; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Loaded Models Extension Loader for Open WebUI | |
| * | |
| * This loader injects the "Loaded Models" sidebar extension into Open WebUI. | |
| * It supports ES6 modules and provides fallback paths for different deployment scenarios. | |
| */ | |
| // Check if browser supports ES6 modules | |
| const supportsModules = 'noModule' in HTMLScriptElement.prototype; | |
| if (supportsModules) { | |
| console.log('[Loaded Models Extension] Loading extension with ES6 modules...'); | |
| const loadApp = () => { | |
| // Version for cache busting | |
| const VERSION = '1.0.0'; | |
| const v = `?v=${VERSION}`; | |
| // Determine base URL from the current script's location | |
| let baseUrl = ''; | |
| try { | |
| if (document.currentScript) { | |
| baseUrl = document.currentScript.src.substring(0, document.currentScript.src.lastIndexOf('/') + 1); | |
| } else { | |
| // Fallback: search for loader.js in script tags | |
| const scripts = document.querySelectorAll('script'); | |
| for (let script of scripts) { | |
| if (script.src && script.src.includes('loader.js')) { | |
| baseUrl = script.src.substring(0, script.src.lastIndexOf('/') + 1); | |
| break; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.warn('[Loaded Models Extension] Could not determine base URL:', e); | |
| } | |
| // Helper to load a module with error handling | |
| const tryLoad = (path, label) => { | |
| let fullPath = path; | |
| if (baseUrl && path.startsWith('./')) { | |
| fullPath = new URL(path.substring(2), baseUrl).href; | |
| } | |
| console.log(`[Loaded Models Extension] Trying to load from ${label}: ${fullPath}`); | |
| return import(fullPath + v).then(module => { | |
| console.log(`[Loaded Models Extension] ✅ Successfully loaded from ${label}`); | |
| return module; | |
| }); | |
| }; | |
| // Standard Path: js/loaded-models-extension/app.js (relative to loader.js) | |
| return tryLoad('./loaded-models-extension-app.js', 'Standard Path') | |
| .catch(() => { | |
| console.warn('[Loaded Models Extension] Standard path failed, trying fallback...'); | |
| // Fallback: app.js in same directory as loader.js | |
| return tryLoad('./app.js', 'Fallback Path (same dir)'); | |
| }) | |
| .catch((err) => { | |
| console.error('[Loaded Models Extension] ❌ All load attempts failed:', err); | |
| throw err; | |
| }); | |
| }; | |
| loadApp() | |
| .then((appModule) => { | |
| console.log('[Loaded Models Extension] App module loaded successfully'); | |
| // Initialize the extension | |
| if (appModule.initLoadedModelsExtension) { | |
| appModule.initLoadedModelsExtension(); | |
| } else if (appModule.default && typeof appModule.default === 'function') { | |
| appModule.default(); | |
| } else { | |
| console.warn('[Loaded Models Extension] No initialization function found'); | |
| } | |
| }) | |
| .catch((error) => { | |
| console.error('[Loaded Models Extension] Failed to load extension:', error); | |
| }); | |
| } else { | |
| // Legacy browsers: Log error (ES5 is not supported) | |
| console.error('[Loaded Models Extension] ES6 modules required. Browser does not support this feature.'); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment