InvokeAI icon indicating copy to clipboard operation
InvokeAI copied to clipboard

[enhancement]: ui mobile design

Open berkut1 opened this issue 2 years ago • 9 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

OS

Windows

GPU

cuda

VRAM

8GB

What version did you experience this issue on?

3.0.1

What happened?

Starting from version 3.0 the web design for mobile devices was broken. In phone view mode, you can only see the left panel.

Screenshots

No response

Additional context

Launch InvokeAI on the PC/Server and go to the website from the phone.

Contact Details

No response

berkut1 avatar Aug 01 '23 09:08 berkut1

Hi, this is not a bug - we haven't implemented a mobile version for v3 yet. I've converted this into an enhancement requesting a mobile design.

For context, v3.0 is a near-complete-rewrite of the entire application. Because the design was being constantly changed, it didn't make sense to attempt to implement a mobile version. Now that things are stabilizing, we can put more time into this.

psychedelicious avatar Aug 03 '23 01:08 psychedelicious

Adding a quick +1. For reference, 2.x did a decent job of degrading on mobile, by vertically stacking the three "columns" of the UI (the left sidebar with all the parameters, the image area, and the gallery) when viewed on narrow screens. A lot of scrolling, but you could still get to everything and use it fairly effectively. I'm terrible at CSS but maybe a CSS wizard could tackle this layout fallback fairly quickly.

Beyond the layout, there are a handful of other things, such as the newer pop-up menus (with the keyboard autocompletion functionality) which are fairly hostile to touch interfaces. They tend to disappear when you try to scroll them, etc.

Anyway, just glad to hear it's on the radar! Keep up the awesome work.

panicsteve avatar Aug 10 '23 18:08 panicsteve

Is it still on radar? I use all web UIs, invoke is very interresting, BUT using it on cellphone is total pain.

DavidSchobl avatar Aug 13 '23 17:08 DavidSchobl

Yes, it is on the radar, but there is no timeline yet.

psychedelicious avatar Aug 14 '23 00:08 psychedelicious

Super keen for this.

Quite often I’m not at my computer but have an idea/concept I’d like to quickly draft up for later refinement.

sammcj avatar Oct 08 '23 12:10 sammcj

i also would love an optimized "lite" version for mobile phones.

my current use-case was to get gallery images easily on my phone to share them via some messenger apps, instead of downloading and getting it on the phone somehow just to quickly share it with a friend.

created this ugly and cheap hack to only display the gallery if you browse to your invokeai instance via http://url-or-ip:9090/#mobile-gallery

https://gist.github.com/mm2srv/6ddeb11054b1e3c1b26e78081396af2e

on iphone, with chrome, you can hold down on the image for 1-2 seconds and it says Image Copy, then you can simply paste it into any messenger and whatever. this makes the turnaround from wanting to share or create an image for a friend so much quicker, while still being in control of where i paste this image, instead of some global cloud sharing feature that might get build.

this solved my current case. but it would be perfect to even being able to use the full (or just text to image) on mobile. i do understand how much work it requires and the work is probably better put on the real app.

mm2srv avatar Oct 13 '23 02:10 mm2srv

Hello everyone.

I apologize for reviving an old thread, but I have been waiting for the mobile version (since 2.3.5.post2) for a long time, and it still hasn't been released. Therefore, I decided to make a similar modification for the latest version of InvokeAI (5.6.0).

To do this, you need to edit the file /opt/invokeai/invokeai/frontend/web/dist/index.html (your path may vary depending on your installation method): After <script type="module" crossorigin src="./assets/index-**YOUR_ID**.js"></script>, add this code (replacing the style names with your own - use inspector for this):

  <script>
    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }
    document.addEventListener("DOMContentLoaded", function() {
      var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
      if (isMobile) {
        waitForElm('#app-panel-group[data-panel-group]').then((appPanelGroup) => {
          appPanelGroup.setAttribute('data-panel-group-direction', 'vertical');
          appPanelGroup.style.flexDirection = 'column';
        });
        waitForElm('#left-main-handle').then((leftMainHandle) => {
          leftMainHandle.setAttribute('data-panel-group-direction', 'vertical');
        });
        waitForElm('#main-right-handle').then((mainRightHandle) => {
          mainRightHandle.setAttribute('data-panel-group-direction', 'vertical');
        });
        Promise.all([
          waitForElm('#app-panel-group'),
          waitForElm('div[data-theme="dark"].css-126fo1u')   // you should replace this to your own css id
        ]).then(([appPanelGroup, elementToMove]) => {
          appPanelGroup.appendChild(elementToMove);
        });
        waitForElm('div[data-theme="dark"].css-126fo1u').then((element) => {   // you should replace this to your own css id
          element.style.flexDirection = 'row';
        });
        waitForElm('div[data-theme="dark"].css-91bg5e').then((element) => {   // you should replace this to your own css id
          element.style.flexDirection = 'row';
          element.style.paddingTop = '0';
        });
      }
    });
  </script>

The block

      var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
      if (isMobile) {
        ...
      }

is optional — it is used to check which device you are using InvokeAI from.

Then your interface will look something like this: Image Image Image It's not easy, but you can even use nodes.

randomboygh avatar Feb 11 '25 11:02 randomboygh

/invokeai/frontend/web/dist/index.html

This is very cool!

~~How do i get those CSS selector ids for my instance?~~ I found a more dynamic way to do the selectors, but would like to know your approach.

~~Some of yours don't apply on my instance and am getting errors~~. I might tweak it later once it works fully as i cant yet fully adapt it to how the screenshots look.

I am adapting your script to make a Greasemonkey script for it for myself but may publish once it works, i will cite you in the copyright notice of course as original author of code, could you DM me your email+name so i can attribute you? also is MIT/Apache2.0 fine?

my email is [email protected]

Many thanks.

JamesClarke7283 avatar Mar 09 '25 17:03 JamesClarke7283

I made a Greasemokey Userscript, based on your implementation. It doesn't work fully yet to how i want but think its worth posting here. I haven't put a license yet because its based on yours, and i dont know what your license is on that code yet. I would recommend MIT or Apache2.0.

// ==UserScript==
// @name         InvokeAI Mobile Optimization PoC
// @version      0.1.0
// @description  Mobile optimization for InvokeAI based on March 2025 PoC
// @author       James Clarke, based on randomboygh's work: (https://github.com/invoke-ai/InvokeAI/issues/4111#issuecomment-2650473566)
// @match        https://my-invokeai-instance.tld/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  function waitForElm(selector) {
    return new Promise((resolve) => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }
      const observer = new MutationObserver((mutations) => {
        if (document.querySelector(selector)) {
          observer.disconnect();
          resolve(document.querySelector(selector));
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
    });
  }

  // Helper function to find elements by their visual characteristics
  function findElementByCharacteristics(characteristics) {
    // Log all theme divs to console to help identify them
    console.log("Theme divs:", document.querySelectorAll("div[data-theme]"));

    // This is where you'd implement logic to find the element
    // For now, we'll return null and rely on manual identification
    return null;
  }

  document.addEventListener("DOMContentLoaded", function () {
    var isMobile =
      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
        navigator.userAgent
      );
    if (isMobile) {
      waitForElm("#app-panel-group[data-panel-group]").then((appPanelGroup) => {
        appPanelGroup.setAttribute("data-panel-group-direction", "vertical");
        appPanelGroup.style.flexDirection = "column";
      });
      waitForElm("#left-main-handle").then((leftMainHandle) => {
        leftMainHandle.setAttribute("data-panel-group-direction", "vertical");
      });
      waitForElm("#main-right-handle").then((mainRightHandle) => {
        mainRightHandle.setAttribute("data-panel-group-direction", "vertical");
      });

      Promise.all([
        waitForElm("#app-panel-group"),
        waitForElm('#invoke-app-tabs > div[data-theme="dark"]'),
      ]).then(([appPanelGroup, elementToMove]) => {
        if (elementToMove) {
          appPanelGroup.appendChild(elementToMove);
        } else {
          console.error("Could not find the element to move");
        }
      });

      waitForElm('button[aria-label="Toggle Right Panel (G)"]').then(
        (toggleButton) => {
          if (toggleButton) {
            toggleButton.click();
            console.log("Toggled right panel");
          } else {
            console.error("Toggle button not found");
          }
        }
      );

      waitForElm('.chakra-image[alt="invoke-logo"]').then((logoImage) => {
        const outerDiv = logoImage
          ? logoImage.closest('div[data-theme="dark"]')
          : null;
        if (outerDiv) {
          outerDiv.style.flexDirection = "row";
        }
      });

      waitForElm('.chakra-button[data-testid="Canvas"]').then(
        (canvasButton) => {
          const innerDiv = canvasButton
            ? canvasButton.closest('div[data-theme="dark"]')
            : null;
          if (innerDiv) {
            innerDiv.style.flexDirection = "row";
            innerDiv.style.paddingTop = "0";
          }
        }
      );
    }
  });
})();

ScreenShots:

Image

Gallery:

Image

JamesClarke7283 avatar Mar 09 '25 19:03 JamesClarke7283

I have reworked the code - it now makes using invokeai on a phone more convenient. Each panel is fixed and can be scrolled. All obstructive elements are hidden. Known bugs:

  • Images in the gallery cannot be scrolled.
  • Switching tabs breaks everything, but refreshing the page fixes it.

(The code can be freely used without crediting me)

  <script>
    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

    document.addEventListener("DOMContentLoaded", function() {
        var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        if (isMobile) {
            waitForElm('div.chakra-popover__popper').then(el => el.remove());
            waitForElm('div[role="tooltip"]').then(el => el.remove());

            waitForElm('#gallery-wrapper-panel > div > div.css-1tqv56i > div > div > div:nth-child(2) > div').then(el => {
                el.removeAttribute('role');
            });

            waitForElm('#invoke-app-tabs').then((invokeAppTabs) => {
                invokeAppTabs.style.flexDirection = 'column';
            });

            waitForElm('#invoke-app-tabs > div.css-126fo1u').then((navContainer) => {
                navContainer.style.flexDirection = 'row';
                navContainer.style.gap = 'var(--invoke-space-4)';
            });

            waitForElm('#invoke-app-tabs > div.css-126fo1u > div.css-91bg5e').then((buttonGroup) => {
                buttonGroup.style.flexDirection = 'row';
                buttonGroup.style.paddingTop = '0';
                buttonGroup.style.gap = 'var(--invoke-space-2)';
            });

            waitForElm('#app-panel-group').then((panelGroup) => {
                panelGroup.style.flexDirection = 'column';
            });

            waitForElm('#left-main-handle').then((handle) => handle.remove());
            waitForElm('#main-right-handle').then((handle) => handle.remove());

            Promise.all([
                waitForElm('#app-panel-group'),
                waitForElm('#invoke-app-tabs > div.css-126fo1u')
            ]).then(([appPanelGroup, navContainer]) => {
                const parentContainer = appPanelGroup.parentElement;
                if (parentContainer) {
                    parentContainer.insertBefore(navContainer, appPanelGroup.nextSibling);
                }
            });

            waitForElm('#main-panel > div.css-18pu13a').then(el => el.remove());
            waitForElm('#main-panel > div.css-1unonvz > div.chakra-button__group.css-ozmh8i').then(el => el.remove());

            Promise.all([
                waitForElm('#main-panel'),
                waitForElm('#right-panel'),
                waitForElm('#left-panel > div > div.css-6izsca > div > div.css-z2ktvc > div > div > div:nth-child(2)')
            ]).then(([mainPanel, rightPanel, container]) => {
                const targetContainer = document.querySelector('#left-panel > div > div.css-6izsca > div > div.css-z2ktvc > div > div > div:nth-child(2) > div.css-1guknb6');
                
                if (targetContainer) {
                    const parent = targetContainer.parentElement;
                    
                    parent.style.display = 'flex';
                    parent.style.flexDirection = 'column';
                    parent.style.gap = 'var(--invoke-space-4)';
                    
                    parent.insertBefore(mainPanel, targetContainer);
                    parent.insertBefore(rightPanel, targetContainer.nextSibling);
                    
                    [mainPanel, rightPanel].forEach(el => {
                        el.style.flexShrink = '0';
                        el.style.minHeight = '100%';
                    });
                }
            });

            waitForElm('#left-panel > div > div.css-6izsca > div > div.css-1wg91wp').then(elementToMove => {
                waitForElm('#left-panel > div > div.css-6izsca > div > div.css-z2ktvc > div > div > div:nth-child(2) > div.css-1guknb6').then(targetContainer => {
                    if (!elementToMove || !targetContainer) return;

                    const originalStyles = {
                        margin: elementToMove.style.margin,
                        order: elementToMove.style.order
                    };

                    targetContainer.insertBefore(elementToMove, targetContainer.firstChild);

                    Object.assign(elementToMove.style, {
                        order: '-1'
                    });

                    console.log('Элемент перемещен:', {
                        element: elementToMove,
                        target: targetContainer,
                        childrenCount: targetContainer.children.length
                    });
                });
            });

            waitForElm('#left-panel > div > div.css-6izsca > div > div > div > div > div:nth-child(2) > div.css-1guknb6').then(targetElement => {
                targetElement.style.cssText += `
                    height: auto !important;
                    overflow: visible !important;
                `;
                
                targetElement.classList.add('custom-height-reset');
                
                const observer = new MutationObserver(() => {
                    if(targetElement.style.height === 'var(--invoke-sizes-full)') {
                        targetElement.style.height = 'auto';
                    }
                });
                
                observer.observe(targetElement, {
                    attributes: true,
                    attributeFilter: ['style']
                });
            });
        }
    });
  </script>

Preview:

https://github.com/user-attachments/assets/b0683b1b-ce84-4d08-a71b-bb6cd6b9cbc4

randomboygh avatar Apr 14 '25 16:04 randomboygh

@psychedelicious is the 3.0 ui re-write complete? This has been a pain point for me for a while now. I'm down to take a stab at making this better (since I already have the venv setup from my other PR).

heathen711 avatar Jun 20 '25 01:06 heathen711

@heathen711 No, the v6 UI update is not complete. You can track progress in #8069.

We are migrating from (react-resizable-panels)[https://github.com/bvaughn/react-resizable-panels] to dockview for layouts. It's a fairly deep change and I'm still frequently changing things drastically, probably a waste of time to do anything other than experiments at this point.

psychedelicious avatar Jun 20 '25 01:06 psychedelicious