gradio-app / gradio

Build and share delightful machine learning apps, all in Python. 🌟 Star to support our work!
http://www.gradio.app
Apache License 2.0
30.78k stars 2.29k forks source link

Support multiple pages in a gradio app #2654

Open pngwn opened 1 year ago

pngwn commented 1 year ago

Is your feature request related to a problem? Please describe.
It would be good to distinct pages for different parts of a gradio app, including the ability to navigate directly to that page using the url. This feature should also add some kind of navigation bar/ structure in order to navigate the application.

Describe the solution you'd like
Something like this would be cool:

import gradio as gr

gr.Blocks():
  gr.Page("My amazing page", route="/"):
    gr.Markdown("# My amazing page")
    gr.Textbox("Page one")
  gr.Page("My second amazing page", route="/page-two"):
    gr.Markdown("# My second amazing page")
    gr.Textbox("Page two")

This would generate an app with a client side router that shows the first page by default with some kind of navigation bar with a link to the first + second pages. Clicking the second page link would change the url and change what is displayed. the urls would be something like:

We could use hashbangs /#!/xxx but i don't know if there is any value in doing that anymore, might help google a bit but I don't know how crawlable spaces are anyway.

Additional context
It is important that this features works well on huggingface spaces, since it is one of our most used platforms. This introduces certain challenges/ limitations as, even though spaces have their own subdomain now, they are typically used via the the spaces chrome embed.

Additionally as we support embedding via the web component, it is important that gradio doesn't have too many opinions about the structure of the URL and works on any subpath.

With this in mind, I think the best solution is a hash-based router, as hashes are inherently very flexible and can be appended to any URL. This will work well in spaces (and can be made really nice with some tweaks to spaces themselves) and will work well when embedded on blogposts etc.

abidlabs commented 1 year ago

Besides direct URLs, what functionality does a gr.Page() allow that a gr.Tab() doesn't? All sorts of interesting cases pop up with pages -- e.g. can pages be nested inside other pages similar to tabs? Can you .load() a Space with pages inside of another Gradio app? We could implement it but I'm not sure if the value is that high?

pngwn commented 1 year ago

Main thing is that it is a different level of hierarchy to tabs. Tabs and Pages are different, one could not replace the other but they fill a similar role. Nested Tabs are a very poor experience for the end user and should be discouraged, tabs inside pages are nice and clean. This shift in hierarchy is also reflected in having a different levels of navigation for Pages vs Tabs, and allows us more flexibility in how we communicate them to the user.

can pages be nested inside other pages similar to tabs?

Probably, nested routing is a thing

Can you .load() a Space with pages inside of another Gradio app?

Probably, it would form a nested route.

Haven't thought about the implementation much and I'm not going to fight for this feature but it has been requested a couple of times. Value is middling, the URL thing makes it very tempting. A cleaner Tab implementation might go someway to addressing the visual side of things, i dislike tabs as they stand.

1lint commented 1 year ago

One option is to mount multiple gradio apps on a single FastAPI object, and treat each mounted gradio app/route as a page in the overall app, I have a barebones example below extending the doc example from https://gradio.app/sharing-your-app/#mounting-within-another-fastapi-app

from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import gradio as gr

app = FastAPI()

HELLO_ROUTE = "/hello"
GOODBYE_ROUTE = "/goodbye"
iframe_dimensions = "height=300px width=1000px"

index_html = f'''
<h1>Put header here</h1>

<h3>
You can mount multiple gradio apps on a single FastAPI object for a multi-page app.
However if you mount a gradio app downstream of another gradio app, the downstream
apps will be stuck loading. 
</h3>

<h3>
So in particular if you mount a gradio app at the index route "/", then all your 
other mounted gradio apps will be stuck loading. But don't worry, you can still embed
your downstream gradio apps into the index route using iframes like I do here. In fact,
you probably want to do this anyway since its your index page, which you want to detail 
more fully with a jinja template. 
For a full example, you can see my <a href=https://yfu.one/>generative avatar webapp</a>
</h3>

<div>
<iframe src={HELLO_ROUTE} {iframe_dimensions}></iframe>
</div>

<div>
<iframe src={GOODBYE_ROUTE} {iframe_dimensions}></iframe>
</div>

'''

@app.get("/", response_class=HTMLResponse)
def index():
    return index_html

hello_app = gr.Interface(lambda x: "Hello, " + x + "!", "textbox", "textbox")
goodbye_app = gr.Interface(lambda x: "Goodbye, " + x + "!", "textbox", "textbox")

app = gr.mount_gradio_app(app, hello_app, path=HELLO_ROUTE)
app = gr.mount_gradio_app(app, goodbye_app, path=GOODBYE_ROUTE)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app)

(btw how do you embed a gist into comment?)

A key observation I've found is that you cannot mount a gradio app downstream of another mounted gradio app, or else the downstream gradio apps will be disabled. But you can still embed your downstream gradio apps into your upstream routes like I do in the example.

This is what I use for my generative avatar webapp https://yfu.one catering to ML/AI art enthusiasts (based on observations of aesthetic trends in social media). I have multiple pages of gradio apps at https://yfu.one/apps/front_ui/, https://yfu.one/apps/full_ui/, https://yfu.one/register, etc.

Realizing I could make my webapp by just serving jinja templates with embedded gradio apps for reactive web components was a breakthrough moment for me after banging my head on react tutorials for some time. I hope the example can be useful for others seeking to turn their gradio app into a more fully featured webapp with just writing Python.

wgong commented 11 months ago

Love to have this enhancement too, To me, Gradio is a framework to GUI-fy a function, FastAPI a framework to Web-API-fy a function, so if a Gradio app instance can be mapped to a route or exposed as an URL-endpoint, it will open up potential to compose complex Gradio app out of simple atomic apps. My use-case is to build a multi-lingual dictionary in Gradio for educational purpose.

kevinknights29 commented 9 months ago

Love to have this enhancement too, To me, Gradio is a framework to GUI-fy a function, FastAPI a framework to Web-API-fy a function, so if a Gradio app instance can be mapped to a route or exposed as an URL-endpoint, it will open up potential to compose complex Gradio app out of simple atomic apps. My use-case is to build a multi-lingual dictionary in Gradio for educational purpose.

Well said @wgong, I agree completely with your statement and the discussion above. In my case, I'm interested in grouping several LLMs Application built with Gradio and having each of those apps under a given route.

baxtrax commented 9 months ago

I also agree with this idea. I can see many use cases where this feature would be advantageous. Additionally, it would allow for more complex uses of Gradio, making it more of a universal GUI framework.

freddyaboulton commented 9 months ago

Should be possible with custom components Soon ™️

guhuajun commented 2 months ago

Greetings,

I am using Starlette (not Fast API) for a long time. I am following the endpoints pattern. (Yes, in Django context, I also prefer Class Based Views) So, I have the mindset to break a whole thing (one api entry uri, with multiple api endpoints) into small parts. When it comes to gradio, it's a little bit strange feeling to put things together for first time.

IMHO, the existing gradio flavor to mitigate such strange feeling is to use TabbedInterface, then separate each tab into different python files.

image

Last week, I was following the OAuth with External Providers example to enable gradio integration with GitHub Enterprise. It's a successful try. Instead of using decorators for defining routing, I am using routes list pattern. For now, it's just redirections to two gradio apps. But it's possible to define a static HTML page as a landing page with different links to different gradio apps.

I am fine with a gradio package without native multi pages support. I respect the project owner design decision (I guess the project target for gradio will not be something like django-cms). Eventually, the existing gradio users will become full stack developers. Then welcome to the Angular/React/Vue jungles. ;)

Index Page

from starlette.endpoints import HTTPEndpoint
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette.routing import Route

class Index(HTTPEndpoint):
    async def get(self, request: Request):
        user = request.session.get('user', None)

        if user:
            return RedirectResponse(url='/gradio/')
        else:
            return RedirectResponse(url='/login/')

routes = [
    Route('/', Index, name='index'),
]

routes.py

from starlette.routing import Mount

from views.api.routes import routes as routes_api
from views.ui.root import routes as routes_root
from views.ui.auth import routes as routes_auth

# when using starlette
routes = [
    Mount('/api', routes=routes_api),
    Mount('/auth', routes=routes_auth),
    Mount('/', routes=routes_root),
]

main.py

# snipped

# do login
app = gr.mount_gradio_app(app, login_blocks, path='/login')

# show gradio page if authentication is done
app = gr.mount_gradio_app(app, main_blocks, path="/gradio",
                          auth_dependency=get_user)

# add addtional routes
for route in routes:
    app.routes.append(route)
brandon8863 commented 19 hours ago

multi_page_app

I'm very new to Gradio, but I agree that some form of a multi-page app example would be nice. This isn't perfect but it does work with the 30 minutes of testing I've put on it :)

Screenshot 2024-07-07 130505