Skip to content

Commit

Permalink
Feat: Allow users to view list of models (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
punitda authored Aug 25, 2024
1 parent 127ab67 commit 057b0e0
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 3 deletions.
11 changes: 10 additions & 1 deletion backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import logging
from contextlib import asynccontextmanager

from typing import Dict, Annotated
from typing import Dict, Annotated, Optional
from urllib.parse import unquote

from fastapi import FastAPI, File, Header, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
Expand Down Expand Up @@ -205,6 +206,14 @@ async def get_workflow_urls(app_name: str):
status_code=500, detail="Internal server error") from e


@app.get("/models", dependencies=[Depends(verify_api_key)])
async def file_browser(path: Optional[str] = None):
decoded_path = unquote(path) if path else ''
command = f"modal volume ls comfyui-models {decoded_path} --json"
files_json = await run_modal_command(command.strip())
return json.loads(files_json)


async def deploy_app(payload: CreateAppPayload):
folder_path = f"/app/builds/{payload.machine_name}"
cp_process = await asyncio.create_subprocess_exec("cp", "-r", "/app/src/template", folder_path)
Expand Down
15 changes: 13 additions & 2 deletions web/app/components/models-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { Model } from "~/lib/types";
import { cn, isValidModelFileName, isValidModelUrl } from "~/lib/utils";
import { CivitAIModelComboBox, loader } from "~/routes/civitai-search/route";
import { useFetcher } from "@remix-run/react";
import { Link, useFetcher } from "@remix-run/react";
import { InformationCircleIcon } from "@heroicons/react/24/outline";

export interface ModelsFormProps {
Expand Down Expand Up @@ -105,10 +105,21 @@ export default function ModelsForm({
</PopoverTrigger>
<PopoverContent className="w-96">
<p className="text-primary/90 text-sm mt-1">
Models are shared between apps. If you&#39;ve already
Models are shared between apps. If you&#39;ve already{" "}
downloaded all the models you need during the previous app
builds you can skip this step :)
</p>
<p className="text-primary/90 text-sm mt-2">
You can find list of all the models you have downloaded{" "}
<Link
to="/models"
className="underline"
target="_blank"
rel="noopener noreferrer"
>
here
</Link>
</p>
</PopoverContent>
</Popover>
</div>
Expand Down
180 changes: 180 additions & 0 deletions web/app/routes/models/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { useLoaderData, useNavigate } from "@remix-run/react";
import { Folder, File, ChevronLeft } from "lucide-react";
import { json, LoaderFunction, redirect } from "@remix-run/node";
import { fetchModelsFileForPath } from "~/server/file";
import { Button } from "~/components/ui/button";
import { formatRelativeTime } from "~/lib/utils";
import { requireAuth } from "~/server/auth";

type FileSystemItem = {
Filename: string;
Type: "dir" | "file";
Size: string;
"Created/Modified": string;
};

type LoaderData = {
items: FileSystemItem[];
currentPath: string;
};

export const loader: LoaderFunction = async (args) => {
const data = await requireAuth(args);
if ("error" in data) {
return redirect("/sign-in");
}

const url = new URL(args.request.url);
const currentPath = url.searchParams.get("path") || "";

try {
const items = await fetchModelsFileForPath(currentPath);
return json({ items, currentPath });
} catch (error) {
console.error(`Error fetching list of models for ${currentPath}`, error);
return json(
{ items: [], currentPath, error: "Failed to fetch items" },
{ status: 500 }
);
}
};

export default function ModelsBrowser() {
const { items, currentPath } = useLoaderData<LoaderData>();
const navigate = useNavigate();

const navigateToFolder = (path: string) => {
navigate(`?path=${encodeURIComponent(path)}`);
};

const navigateBack = () => {
navigate(-1);
};

const isRootDirectory = currentPath === "";

return (
<div className="px-16 lg:px-32 mt-32">
<div className="sm:flex sm:items-center">
<div className="flex items-center">
{!isRootDirectory && (
<Button
variant="ghost"
size="sm"
className="mr-2 flex-shrink-0"
onClick={navigateBack}
>
<ChevronLeft size={16} />
</Button>
)}
<div>
<h1 className="text-base font-semibold leading-6 text-primary/90">
{isRootDirectory ? "Models" : currentPath.split("/").pop()}
</h1>
{isRootDirectory ? (
<p className="mt-2 text-sm text-primary/90">
A list of downloaded models stored in{" "}
<span>
<a
href="https://modal.com/docs/guide/volumes"
target="_blank"
rel="noreferrer noopener"
className="underline"
>
Modal.Volume
</a>
</span>{" "}
shared between your apps.
</p>
) : null}
</div>
</div>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-accent/50">
<tr>
<th
scope="col"
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-primary sm:pl-6"
>
Name
</th>
<th
scope="col"
className="hidden sm:table-cell px-3 py-3.5 text-left text-sm font-semibold text-primary"
>
Type
</th>
<th
scope="col"
className="hidden sm:table-cell px-3 py-3.5 text-left text-sm font-semibold text-primary"
>
Last Modified
</th>
<th
scope="col"
className="hidden sm:table-cell px-3 py-3.5 text-left text-sm font-semibold text-primary"
>
Size
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{items.length > 0 ? (
items.map((item) => (
<tr
key={item.Filename}
className="hover:bg-accent cursor-pointer"
onClick={() =>
item.Type === "dir" && navigateToFolder(item.Filename)
}
>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-primary sm:pl-6">
<div className="flex items-center">
{item.Type === "dir" ? (
<Folder
size={16}
className="mr-2 text-primary flex-shrink-0"
/>
) : (
<File
size={16}
className="mr-2 text-primary/90 flex-shrink-0"
/>
)}
<span className="truncate">
{item.Filename.split("/").pop()}
</span>
</div>
</td>
<td className="hidden sm:table-cell whitespace-nowrap px-3 py-4 text-sm text-primary">
{item.Type === "dir" ? "Directory" : "File"}
</td>
<td className="hidden sm:table-cell whitespace-nowrap px-3 py-4 text-sm text-primary">
{formatRelativeTime(item["Created/Modified"])}
</td>
<td className="hidden sm:table-cell whitespace-nowrap px-3 py-4 text-sm text-primary">
{item.Type === "file" && item.Size ? item.Size : ""}
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="py-4 text-center text-primary">
No files
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions web/app/server/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CloudCog } from "lucide-react";

export type FileSystemItem = {
Filename: string;
Type: "dir" | "file";
Size?: string;
};

export async function fetchModelsFileForPath(
path: string
): Promise<FileSystemItem[]> {
const baseUrl = process.env.APP_BUILDER_API_BASE_URL;

const url = new URL(`${baseUrl}/models`);
if (path !== "") {
url.searchParams.append("path", encodeURIComponent(path));
}

const response = await fetch(url.toString(), {
method: "GET",
headers: {
X_API_KEY: process.env.APP_BUILDER_API_KEY!,
},
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return data;
}

0 comments on commit 057b0e0

Please sign in to comment.