diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..c0239352 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,84 @@ +# ComfyUI LoRA Manager + +## Project Overview + +ComfyUI LoRA Manager is a comprehensive extension for ComfyUI that streamlines the organization, downloading, and application of LoRA models. It functions as both a custom node within ComfyUI and a standalone application. + +**Key Features:** +* **Model Management:** Browse, organize, and download LoRA models (and Checkpoints/Embeddings) from Civitai and CivArchive. +* **Visualization:** Preview images, videos, and trigger words. +* **Workflow Integration:** "One-click" integration into ComfyUI workflows, preserving generation parameters. +* **Recipe System:** Save and share LoRA combinations as "recipes". +* **Architecture:** Hybrid Python backend (API, file management) and JavaScript/HTML frontend (Web UI). + +## Directory Structure + +* `py/`: Core Python backend source code. + * `lora_manager.py`: Main entry point for the ComfyUI node. + * `routes/`: API route definitions (using `aiohttp` in standalone, or ComfyUI's server). + * `services/`: Business logic (downloading, metadata, scanning). + * `nodes/`: ComfyUI custom node implementations. +* `static/`: Frontend static assets (CSS, JS, Images). +* `templates/`: HTML templates (Jinja2). +* `locales/`: Internationalization JSON files. +* `web/comfyui/`: JavaScript extensions specifically for the ComfyUI interface. +* `standalone.py`: Entry point for running the manager as a standalone web app. +* `tests/`: Backend tests. +* `requirements.txt`: Python runtime dependencies. +* `package.json`: Frontend development dependencies and test scripts. + +## Building and Running + +### Prerequisites +* Python 3.8+ +* Node.js (only for running frontend tests) + +### Backend Setup +1. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` + +### Running in Standalone Mode +You can run the manager independently of ComfyUI for development or management purposes. +```bash +python standalone.py --port 8188 +``` + +### Running in ComfyUI +Ensure the folder is located in `ComfyUI/custom_nodes/`. ComfyUI will automatically load it upon startup. + +## Testing + +### Backend Tests (Pytest) +1. Install development dependencies: + ```bash + pip install -r requirements-dev.txt + ``` +2. Run tests: + ```bash + pytest + ``` + * Coverage reports are generated in `coverage/backend/`. + +### Frontend Tests (Vitest) +1. Install Node dependencies: + ```bash + npm install + ``` +2. Run tests: + ```bash + npm run test + ``` +3. Run coverage: + ```bash + npm run test:coverage + ``` + +## Development Conventions + +* **Python Style:** Follow PEP 8. Use snake_case for files/functions and PascalCase for classes. +* **Frontend:** Standard ES modules. UI components often end in `_widget.js`. +* **Configuration:** User settings are stored in `settings.json`. Developers should reference `settings.json.example`. +* **Localization:** Update `locales/.json` and run `scripts/sync_translation_keys.py` when changing UI text. +* **Documentation:** Architecture details are in `docs/architecture/` and `IFLOW.md`. diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index b07bac01..babf5b63 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -61,6 +61,37 @@ class ModelPageView: self._settings = settings_service self._server_i18n = server_i18n self._logger = logger + self._app_version = self._get_app_version() + + def _get_app_version(self) -> str: + version = "1.0.0" + short_hash = "stable" + try: + import toml + current_file = os.path.abspath(__file__) + # Navigate up from py/routes/handlers/model_handlers.py to project root + root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_file)))) + pyproject_path = os.path.join(root_dir, 'pyproject.toml') + + if os.path.exists(pyproject_path): + with open(pyproject_path, 'r', encoding='utf-8') as f: + data = toml.load(f) + version = data.get('project', {}).get('version', '1.0.0').replace('v', '') + + # Try to get git info for granular cache busting + git_dir = os.path.join(root_dir, '.git') + if os.path.exists(git_dir): + try: + import git + repo = git.Repo(root_dir) + short_hash = repo.head.commit.hexsha[:7] + except Exception: + # Fallback if git is not available or not a repo + pass + except Exception as e: + self._logger.debug(f"Failed to read version info for cache busting: {e}") + + return f"{version}-{short_hash}" async def handle(self, request: web.Request) -> web.Response: try: @@ -96,6 +127,7 @@ class ModelPageView: "request": request, "folders": [], "t": self._server_i18n.get_translation, + "version": self._app_version, } if not is_initializing: diff --git a/static/js/core.js b/static/js/core.js index 9701a32f..11c09a1a 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -84,10 +84,7 @@ export class AppCore { // Start onboarding if needed (after everything is initialized) setTimeout(() => { - // Do not show onboarding if version-mismatch banner is visible - if (!bannerService.isBannerVisible('version-mismatch')) { - onboardingManager.start(); - } + onboardingManager.start(); }, 1000); // Small delay to ensure all elements are rendered // Return the core instance for chaining diff --git a/static/js/managers/BannerService.js b/static/js/managers/BannerService.js index f4461fdc..5b0947a9 100644 --- a/static/js/managers/BannerService.js +++ b/static/js/managers/BannerService.js @@ -17,7 +17,7 @@ const AFDIAN_URL = 'https://afdian.com/a/pixelpawsai'; const BANNER_HISTORY_KEY = 'banner_history'; const BANNER_HISTORY_VIEWED_AT_KEY = 'banner_history_viewed_at'; const BANNER_HISTORY_LIMIT = 20; -const HISTORY_EXCLUDED_IDS = new Set(['version-mismatch']); +const HISTORY_EXCLUDED_IDS = new Set([]); /** * Banner Service for managing notification banners diff --git a/static/js/managers/UpdateService.js b/static/js/managers/UpdateService.js index d93784fa..0cfb7619 100644 --- a/static/js/managers/UpdateService.js +++ b/static/js/managers/UpdateService.js @@ -4,8 +4,7 @@ import { setStorageItem, getStoredVersionInfo, setStoredVersionInfo, - isVersionMatch, - resetDismissedBanner + isVersionMatch } from '../utils/storageHelpers.js'; import { bannerService } from './BannerService.js'; import { translate } from '../utils/i18nHelpers.js'; @@ -753,94 +752,14 @@ export class UpdateService { stored: getStoredVersionInfo() }); - // Reset dismissed status for version mismatch banner - resetDismissedBanner('version-mismatch'); - - // Register and show the version mismatch banner - this.registerVersionMismatchBanner(); + // Silently update stored version info as cache busting handles the resource updates + setStoredVersionInfo(this.currentVersionInfo); } } } catch (error) { console.error('Failed to check version info:', error); } } - - registerVersionMismatchBanner() { - // Get stored and current version for display - const storedVersion = getStoredVersionInfo() || translate('common.status.unknown'); - const currentVersion = this.currentVersionInfo || translate('common.status.unknown'); - - bannerService.registerBanner('version-mismatch', { - id: 'version-mismatch', - title: translate('banners.versionMismatch.title', {}, 'Application Update Detected'), - content: translate('banners.versionMismatch.content', { - storedVersion, - currentVersion - }, `Your browser is running an outdated version of LoRA Manager (${storedVersion}). The server has been updated to version ${currentVersion}. Please refresh to ensure proper functionality.`), - actions: [ - { - text: translate('banners.versionMismatch.refreshNow', {}, 'Refresh Now'), - icon: 'fas fa-sync', - action: 'hardRefresh', - type: 'primary' - } - ], - dismissible: false, - priority: 10, - countdown: 15, - onRegister: (bannerElement) => { - // Add countdown element - const countdownEl = document.createElement('div'); - countdownEl.className = 'banner-countdown'; - countdownEl.innerHTML = `${translate('banners.versionMismatch.refreshingIn', {}, 'Refreshing in')} 15 ${translate('banners.versionMismatch.seconds', {}, 'seconds')}...`; - bannerElement.querySelector('.banner-content').appendChild(countdownEl); - - // Start countdown - let seconds = 15; - const countdownInterval = setInterval(() => { - seconds--; - const strongEl = countdownEl.querySelector('strong'); - if (strongEl) strongEl.textContent = seconds; - - if (seconds <= 0) { - clearInterval(countdownInterval); - this.performHardRefresh(); - } - }, 1000); - - // Store interval ID for cleanup - bannerElement.dataset.countdownInterval = countdownInterval; - - // Add action button event handler - const actionBtn = bannerElement.querySelector('.banner-action[data-action="hardRefresh"]'); - if (actionBtn) { - actionBtn.addEventListener('click', (e) => { - e.preventDefault(); - clearInterval(countdownInterval); - this.performHardRefresh(); - }); - } - }, - onRemove: (bannerElement) => { - // Clear any existing interval - const intervalId = bannerElement.dataset.countdownInterval; - if (intervalId) { - clearInterval(parseInt(intervalId)); - } - } - }); - } - - performHardRefresh() { - // Update stored version info before refreshing - setStoredVersionInfo(this.currentVersionInfo); - - // Force a hard refresh by adding cache-busting parameter - const cacheBuster = new Date().getTime(); - window.location.href = window.location.pathname + - (window.location.search ? window.location.search + '&' : '?') + - `cache=${cacheBuster}`; - } } // Create and export singleton instance diff --git a/templates/base.html b/templates/base.html index b3beeff2..573e2112 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,8 +4,8 @@ {% block title %}{{ t('header.appTitle') }}{% endblock %} - - + + {% block page_css %}{% endblock %} - + {% else %} {% block main_script %}{% endblock %} {% endif %} diff --git a/templates/checkpoints.html b/templates/checkpoints.html index ffa30aff..fd3498da 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -40,5 +40,5 @@ {% endblock %} {% block main_script %} - + {% endblock %} diff --git a/templates/embeddings.html b/templates/embeddings.html index badf12e3..de8b807a 100644 --- a/templates/embeddings.html +++ b/templates/embeddings.html @@ -40,5 +40,5 @@ {% endblock %} {% block main_script %} - + {% endblock %} diff --git a/templates/loras.html b/templates/loras.html index 3ece6a10..5ede68d9 100644 --- a/templates/loras.html +++ b/templates/loras.html @@ -24,6 +24,6 @@ {% block main_script %} {% if not is_initializing %} - + {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/recipes.html b/templates/recipes.html index 58003607..202791a2 100644 --- a/templates/recipes.html +++ b/templates/recipes.html @@ -4,9 +4,9 @@ {% block page_id %}recipes{% endblock %} {% block page_css %} - - - + + + {% endblock %} {% block additional_components %} @@ -84,5 +84,5 @@ {% endblock %} {% block main_script %} - + {% endblock %} \ No newline at end of file diff --git a/templates/statistics.html b/templates/statistics.html index e8d73ed7..1986ac3e 100644 --- a/templates/statistics.html +++ b/templates/statistics.html @@ -192,6 +192,6 @@ {% block main_script %} {% if not is_initializing %} - + {% endif %} {% endblock %} \ No newline at end of file diff --git a/tests/frontend/core/appCore.test.js b/tests/frontend/core/appCore.test.js index 094200f1..ad3d4bf6 100644 --- a/tests/frontend/core/appCore.test.js +++ b/tests/frontend/core/appCore.test.js @@ -234,7 +234,6 @@ describe('AppCore initialization flow', () => { await vi.runAllTimersAsync(); expect(onboardingManager.start).toHaveBeenCalledTimes(1); - expect(bannerService.isBannerVisible).toHaveBeenCalledWith('version-mismatch'); }); it('does not reinitialize once initialized', async () => { @@ -262,13 +261,4 @@ describe('AppCore initialization flow', () => { expect(BulkContextMenu).not.toHaveBeenCalled(); expect(bulkManager.setBulkContextMenu).not.toHaveBeenCalled(); }); - - it('suppresses onboarding when version mismatch banner is visible', async () => { - bannerService.isBannerVisible.mockReturnValueOnce(true); - - await appCore.initialize(); - await vi.runAllTimersAsync(); - - expect(onboardingManager.start).not.toHaveBeenCalled(); - }); });