Compare commits

...
Author SHA1 Message Date
Nicholas Koben Kao ed659ebdb8 schema 2024-01-22 19:05:24 -08:00
Nicholas Koben Kao f6a1b88dda rest 2024-01-22 19:05:18 -08:00
Nicholas Koben Kao fed7b380b6 work 2024-01-22 17:23:35 -08:00
Nicholas Koben Kao 90cec6b778 schema 2024-01-20 22:01:11 -08:00
Nicholas Koben Kao 7b61fea849 volume work 2024-01-20 19:54:35 -08:00
Nicholas Koben Kao 60471a8e01 model in voluems 2024-01-20 12:53:48 -08:00
BennyKok c98a16a2dd feat: add duration display 2024-01-20 17:46:15 +08:00
BennyKok 1a0d73ff8b fix: unable to open deployment code template 2024-01-20 14:27:01 +08:00
BennyKok 163e6f0426 feat: add share page settings dialog + add org id to deployment as well 2024-01-20 12:06:26 +08:00
60 changed files with 5940 additions and 405 deletions
+51 -1
View File
@@ -8,6 +8,7 @@ from enum import Enum
import json import json
import subprocess import subprocess
import time import time
from uuid import uuid4
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import asyncio import asyncio
import threading import threading
@@ -19,6 +20,7 @@ from urllib.parse import parse_qs
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp, Scope, Receive, Send from starlette.types import ASGIApp, Scope, Receive, Send
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
# executor = ThreadPoolExecutor(max_workers=5) # executor = ThreadPoolExecutor(max_workers=5)
@@ -224,6 +226,52 @@ async def websocket_endpoint(websocket: WebSocket, machine_id: str):
# return {"Hello": "World"} # return {"Hello": "World"}
class UploadBody(BaseModel):
download_url: str
volume_name: str
volume_id: str
# callback_url: str
@app.post("/upload_volume")
async def upload_checkpoint(body: UploadBody):
global last_activity_time
last_activity_time = time.time()
logger.info(f"Extended inactivity time to {global_timeout}")
download_url = body.download_url
volume_name = body.volume_name
# callback_url = body.callback_url
folder_path = f"/app/builds/{body.volume_id}"
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume-builder", folder_path)
await cp_process.wait()
# Write the config file
config = {
"volume_names": {
volume_name: download_url
},
"paths": {
volume_name: f'/volumes/{uuid4()}'
},
}
await asyncio.subprocess.create_subprocess_shell(
f"modal run app.py",
# stdout=asyncio.subprocess.PIPE,
# stderr=asyncio.subprocess.PIPE,
cwd=folder_path,
env={**os.environ, "COLUMNS": "10000"}
)
with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config))
# check that thi
return JSONResponse(status_code=200, content={"message": "Volume uploading", "build_machine_instance_id": fly_instance_id})
@app.post("/create") @app.post("/create")
async def create_machine(item: Item): async def create_machine(item: Item):
global last_activity_time global last_activity_time
@@ -312,7 +360,9 @@ async def build_logic(item: Item):
config = { config = {
"name": item.name, "name": item.name,
"deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"), "deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"),
"gpu": item.gpu "gpu": item.gpu,
"public_checkpoint_volume": "model-store",
"private_checkpoint_volume": "private-model-store"
} }
with open(f"{folder_path}/config.py", "w") as f: with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config)) f.write("config = " + json.dumps(config))
+12 -7
View File
@@ -1,12 +1,13 @@
from config import config from config import config
import modal import modal
from modal import Image, Mount, web_endpoint, Stub, asgi_app from modal import Image, Mount, web_endpoint, Stub, asgi_app, Volume
import json import json
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from volume import volumes
# deploy_test = False # deploy_test = False
@@ -28,7 +29,6 @@ web_app = FastAPI()
print(config) print(config)
print("deploy_test ", deploy_test) print("deploy_test ", deploy_test)
stub = Stub(name=config["name"]) stub = Stub(name=config["name"])
# print(stub.app_id)
if not deploy_test: if not deploy_test:
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data")) # dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
@@ -56,7 +56,7 @@ if not deploy_test:
# # Install comfy deploy # # Install comfy deploy
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git", # "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
# ) # )
# .copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui") .copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui")
.copy_local_file(f"{current_directory}/data/start.sh", "/start.sh") .copy_local_file(f"{current_directory}/data/start.sh", "/start.sh")
.run_commands("chmod +x /start.sh") .run_commands("chmod +x /start.sh")
@@ -153,8 +153,9 @@ image = Image.debian_slim()
target_image = image if deploy_test else dockerfile_image target_image = image if deploy_test else dockerfile_image
@stub.function(image=target_image, gpu=config["gpu"]
@stub.function(image=target_image, gpu=config["gpu"]) ,volumes=volumes
)
def run(input: Input): def run(input: Input):
import subprocess import subprocess
import time import time
@@ -163,6 +164,7 @@ def run(input: Input):
command = ["python", "main.py", command = ["python", "main.py",
"--disable-auto-launch", "--disable-metadata"] "--disable-auto-launch", "--disable-metadata"]
server_process = subprocess.Popen(command, cwd="/comfyui") server_process = subprocess.Popen(command, cwd="/comfyui")
check_server( check_server(
@@ -235,7 +237,9 @@ async def bar(request_input: RequestInput):
# pass # pass
@stub.function(image=image) @stub.function(image=image
,volumes=volumes
)
@asgi_app() @asgi_app()
def comfyui_api(): def comfyui_api():
return web_app return web_app
@@ -285,6 +289,7 @@ def spawn_comfyui_in_background():
# to be on a single container. # to be on a single container.
concurrency_limit=1, concurrency_limit=1,
timeout=10 * 60, timeout=10 * 60,
volumes=volumes,
) )
@asgi_app() @asgi_app()
def comfyui_app(): def comfyui_app():
@@ -303,4 +308,4 @@ def comfyui_app():
}, },
)() )()
return make_simple_proxy_app(ProxyContext(config)) return make_simple_proxy_app(ProxyContext(config))
+7 -1
View File
@@ -1 +1,7 @@
config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"} config = {
"name": "my-app",
"deploy_test": "True",
"gpu": "T4",
"public_checkpoint_volume": "model-store",
"private_checkpoint_volume": "private-model-store"
}
@@ -1,11 +1,30 @@
comfyui: comfyui:
base_path: /runpod-volume/ComfyUI/ base_path: /extra_models/
checkpoints: models/checkpoints/ checkpoints: |
clip: models/clip/ checkpoints
clip_vision: models/clip_vision/ private_checkpoints
configs: models/configs/ clip: |
controlnet: models/controlnet/ clip
embeddings: models/embeddings/ private_clip
loras: models/loras/ clip_vision: |
upscale_models: models/upscale_models/ clip_vision
vae: models/vae/ private_clip_vision
configs: |
configs
private_configs
controlnet: |
controlnet
private_controlnet
embeddings: |
embeddings
private_embeddings
loras: |
loras
private_loras
upscale_models: |
upscale_models
private_upscale_models
vae: |
vae
private_vae
@@ -0,0 +1,101 @@
"""
This is a standalone script to download models into a modal Volume using civitai
Example Usage
`modal run insert_models::insert_model --civitai-url https://civitai.com/models/36520/ghostmix`
This inserts an individual model from a civitai url
`modal run insert_models::insert_models_civitai_api`
This inserts a bunch of models based on the models retrieved by civitai
civitai's API reference https://github.com/civitai/civitai/wiki/REST-API-Reference
"""
import modal
import subprocess
import requests
import json
stub = modal.Stub()
# NOTE: volume name can be variable
volume = modal.Volume.persisted("rah")
model_store_path = "/vol/models"
MODEL_ROUTE = "models"
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
@stub.function(volumes={model_store_path: volume}, image=image, timeout=50000, gpu=None)
def download_model(download_url):
print(download_url)
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volume.commit()
# file is raw output from Civitai API https://github.com/civitai/civitai/wiki/REST-API-Reference
@stub.function()
def get_civitai_models(model_type: str, sort: str = "Highest Rated", page: int = 1):
"""Fetch models from CivitAI API based on type."""
try:
response = requests.get(f"https://civitai.com/api/v1/models", params={"types": model_type, "page": page, "sort": sort})
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Error fetching models: {e}")
return None
@stub.function()
def get_civitai_model_url(civitai_url: str):
# Validate the URL
if civitai_url.startswith("https://civitai.com/api/"):
api_url = civitai_url
elif civitai_url.startswith("https://civitai.com/models/"):
try:
model_id = civitai_url.split("/")[4]
int(model_id)
except (IndexError, ValueError):
return None
api_url = f"https://civitai.com/api/v1/models/{model_id}"
else:
return "Error: URL must be from civitai.com and contain /models/"
response = requests.get(api_url)
# Check for successful response
if response.status_code != 200:
return f"Error: Unable to fetch data from {api_url}"
# Return the response data
return response.json()
@stub.local_entrypoint()
def insert_models_civitai_api(type: str = "Checkpoint", sort = "Highest Rated", page: int = 1):
civitai_models = get_civitai_models.local(type, sort, page)
if civitai_models:
for _ in download_model.map(map(lambda model: model['modelVersions'][0]['downloadUrl'], civitai_models['items'])):
pass
else:
print("Failed to retrieve models.")
@stub.local_entrypoint()
def insert_model(civitai_url: str):
if civitai_url.startswith("'https://civitai.com/api/download/models/"):
download_url = civitai_url
else:
civitai_model = get_civitai_model_url.local(civitai_url)
if civitai_model:
download_url = civitai_model['modelVersions'][0]['downloadUrl']
else:
return "invalid URL"
download_model.remote(download_url)
@stub.local_entrypoint()
def simple_download():
download_urls = ['https://civitai.com/api/download/models/119057', 'https://civitai.com/api/download/models/130090', 'https://civitai.com/api/download/models/31859', 'https://civitai.com/api/download/models/128713', 'https://civitai.com/api/download/models/179657', 'https://civitai.com/api/download/models/143906', 'https://civitai.com/api/download/models/9208', 'https://civitai.com/api/download/models/136078', 'https://civitai.com/api/download/models/134065', 'https://civitai.com/api/download/models/288775', 'https://civitai.com/api/download/models/95263', 'https://civitai.com/api/download/models/288982', 'https://civitai.com/api/download/models/87153', 'https://civitai.com/api/download/models/10638', 'https://civitai.com/api/download/models/263809', 'https://civitai.com/api/download/models/130072', 'https://civitai.com/api/download/models/117019', 'https://civitai.com/api/download/models/95256', 'https://civitai.com/api/download/models/197181', 'https://civitai.com/api/download/models/256915', 'https://civitai.com/api/download/models/118945', 'https://civitai.com/api/download/models/125843', 'https://civitai.com/api/download/models/179015', 'https://civitai.com/api/download/models/245598', 'https://civitai.com/api/download/models/223670', 'https://civitai.com/api/download/models/90072', 'https://civitai.com/api/download/models/290817', 'https://civitai.com/api/download/models/154097', 'https://civitai.com/api/download/models/143497', 'https://civitai.com/api/download/models/5637']
for _ in download_model.map(download_urls):
pass
@@ -45,13 +45,13 @@ for package in packages:
response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers) response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers)
print(response.text) print(response.text)
with open('models.json') as f: # with open('models.json') as f:
models = json.load(f) # models = json.load(f)
#
for model in models: # for model in models:
response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers) # response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
print(response.text) # print(response.text)
# Close the server # Close the server
server_process.terminate() server_process.terminate()
print("Finished installing dependencies.") print("Finished installing dependencies.")
@@ -0,0 +1,10 @@
import modal
from config import config
public_model_volume = modal.Volume.persisted(config["public_checkpoint_volume"])
private_volume = modal.Volume.persisted(config["private_checkpoint_volume"])
BASEMODEL_DIR = "/extra_models/"
MODEL_DIR = BASEMODEL_DIR + "checkpoints"
PRIVATE_MODEL_DIR = BASEMODEL_DIR + "private_checkpoints"
volumes = {MODEL_DIR: public_model_volume, PRIVATE_MODEL_DIR: private_volume}
@@ -0,0 +1,45 @@
import modal
from config import config
import os
import subprocess
stub = modal.Stub()
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
def is_valid_name(name: str) -> bool:
allowed_characters = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._")
return 0 < len(name) <= 64 and all(char in allowed_characters for char in name)
def create_volumes(volume_names, paths):
path_to_vol = {}
for volume_name in volume_names.keys():
if not is_valid_name(volume_name):
pass
modal_volume = modal.Volume.persisted(volume_name)
path_to_vol[paths[volume_name]] = modal_volume
return path_to_vol
vol_name_to_links = config["volume_names"]
vol_name_to_path = config["paths"]
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
print(vol_name_to_links)
print(vol_name_to_path)
print(volumes)
@stub.function(volumes=volumes, image=image, timeout=5000, gpu=None)
def download_model(volume_name, download_url):
model_store_path = vol_name_to_path[volume_name]
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volumes[model_store_path].commit()
@stub.local_entrypoint()
def simple_download():
print(vol_name_to_links)
print([(vol_name, link) for vol_name,link in vol_name_to_links.items()])
list(download_model.starmap([(vol_name, link) for vol_name,link in vol_name_to_links.items()]))
@@ -0,0 +1,8 @@
config = {
"volume_names": {
"test": "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose (1).png"
},
"paths": {
"test": "/volumes/something"
}
}
BIN
View File
Binary file not shown.
@@ -0,0 +1,2 @@
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "description" text;--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "showcase_media" jsonb;
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "org_id" text;
+62
View File
@@ -0,0 +1,62 @@
DO $$ BEGIN
CREATE TYPE "model_upload_type" AS ENUM('civitai', 'huggingface', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "resource_upload" AS ENUM('started', 'failed', 'succeded');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoints" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"description" text,
"checkpoint_volume_id" uuid NOT NULL,
"model_name" text,
"civitai_id" text,
"civitai_version_id" text,
"civitai_url" text,
"civitai_download_url" text,
"civitai_model_response" jsonb,
"hf_url" text,
"s3_url" text,
"client_url" text,
"is_public" boolean DEFAULT false NOT NULL,
"status" "resource_upload" DEFAULT 'started' NOT NULL,
"upload_machine_id" text,
"upload_type" "model_upload_type" NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoint_volume" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"volume_name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"disabled" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_checkpoint_volume_id_workflow_runs_id_fk" FOREIGN KEY ("checkpoint_volume_id") REFERENCES "comfyui_deploy"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoint_volume" ADD CONSTRAINT "checkpoint_volume_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
+2
View File
@@ -0,0 +1,2 @@
ALTER TYPE "resource_upload" ADD VALUE 'error';--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."checkpoints" ADD COLUMN "build_log" text;
+750
View File
@@ -0,0 +1,750 @@
{
"id": "a7d6a8dd-0e15-4165-98a2-de2a334455dc",
"prevId": "7bdeb193-ee27-40cc-8252-59ddaf505ab8",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
+756
View File
@@ -0,0 +1,756 @@
{
"id": "db06ea66-92c2-4ebe-93c1-6cb8a90ccd8b",
"prevId": "a7d6a8dd-0e15-4165-98a2-de2a334455dc",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+28
View File
@@ -204,6 +204,34 @@
"when": 1705642345817, "when": 1705642345817,
"tag": "0028_futuristic_lady_deathstrike", "tag": "0028_futuristic_lady_deathstrike",
"breakpoints": true "breakpoints": true
},
{
"idx": 29,
"version": "5",
"when": 1705662714161,
"tag": "0029_large_frightful_four",
"breakpoints": true
},
{
"idx": 30,
"version": "5",
"when": 1705716303820,
"tag": "0030_kind_doorman",
"breakpoints": true
},
{
"idx": 31,
"version": "5",
"when": 1705975916818,
"tag": "0031_safe_multiple_man",
"breakpoints": true
},
{
"idx": 32,
"version": "5",
"when": 1705979098372,
"tag": "0032_material_wallflower",
"breakpoints": true
} }
] ]
} }
+5 -4
View File
@@ -1,4 +1,3 @@
import million from 'million/compiler';
import { recmaPlugins } from "./src/mdx/recma.mjs"; import { recmaPlugins } from "./src/mdx/recma.mjs";
import { rehypePlugins } from "./src/mdx/rehype.mjs"; import { rehypePlugins } from "./src/mdx/rehype.mjs";
import { remarkPlugins } from "./src/mdx/remark.mjs"; import { remarkPlugins } from "./src/mdx/remark.mjs";
@@ -21,6 +20,8 @@ const nextConfig = {
}, },
}; };
export default million.next( export default withSearch(withMDX(nextConfig));
withSearch(withMDX(nextConfig)), { auto: { rsc: true } }
); // export default million.next(
// withSearch(withMDX(nextConfig)), { auto: { rsc: true } }
// );
+1
View File
@@ -60,6 +60,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"embla-carousel-react": "^8.0.0-rc19",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"flexsearch": "^0.7.31", "flexsearch": "^0.7.31",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
@@ -0,0 +1,9 @@
import { SharePageSettings } from "@/components/SharePageSettings";
export default async function Page({
params,
}: {
params: { share_id: string };
}) {
return <SharePageSettings deployment_id={params.share_id} />;
}
+7
View File
@@ -0,0 +1,7 @@
import type { FC } from "react";
const Default: FC = () => {
return null;
};
export default Default;
@@ -0,0 +1,50 @@
import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { db } from "@/db/db";
import { checkpointTable, machinesTable } from "@/db/schema";
import { eq } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
const Request = z.object({
machine_id: z.string(),
endpoint: z.string().optional(),
build_log: z.string().optional(),
});
export async function POST(request: Request) {
const [data, error] = await parseDataSafe(Request, request);
if (!data || error) return error;
// console.log(data);
const { machine_id, endpoint, build_log } = data;
if (endpoint) {
await db
.update(checkpointTable)
.set({
// status: "ready",
// endpoint: endpoint,
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
} else {
// console.log(data);
await db
.update(machinesTable)
.set({
// status: "error",
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
}
return NextResponse.json(
{
message: "success",
},
{
status: 200,
}
);
}
+5 -3
View File
@@ -1,15 +1,14 @@
import { Navbar } from "../../components/Navbar"; import { Navbar } from "../../components/Navbar";
import "./globals.css"; import "./globals.css";
import { PHProvider } from "./providers";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ClerkProvider } from "@clerk/nextjs"; import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next"; import type { Metadata } from "next";
import meta from "next-gen/config"; import meta from "next-gen/config";
import PlausibleProvider from "next-plausible"; import PlausibleProvider from "next-plausible";
import dynamic from "next/dynamic";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { PHProvider } from "./providers";
import dynamic from "next/dynamic";
const PostHogPageView = dynamic(() => import("./PostHogPageView"), { const PostHogPageView = dynamic(() => import("./PostHogPageView"), {
ssr: false, ssr: false,
@@ -34,8 +33,10 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
modal,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
modal: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
@@ -60,6 +61,7 @@ export default function RootLayout({
{children} {children}
</div> </div>
<Toaster richColors /> <Toaster richColors />
{modal}
</main> </main>
</body> </body>
</PHProvider> </PHProvider>
+7 -4
View File
@@ -1,8 +1,6 @@
import { ButtonActionMenu } from "@/components/ButtonActionLoader"; import { ButtonActionMenu } from "@/components/ButtonActionLoader";
import {
PublicRunOutputs,
} from "@/components/VersionSelect";
import { RunWorkflowInline } from "@/components/RunWorkflowInline"; import { RunWorkflowInline } from "@/components/RunWorkflowInline";
import { PublicRunOutputs } from "@/components/VersionSelect";
import { import {
Card, Card,
CardContent, CardContent,
@@ -89,6 +87,11 @@ export default async function Page({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div>
{sharedDeployment?.description && (
<>{sharedDeployment?.description}</>
)}
</div>
<RunWorkflowInline <RunWorkflowInline
inputs={inputs} inputs={inputs}
machine_id={sharedDeployment.machine_id} machine_id={sharedDeployment.machine_id}
@@ -102,7 +105,7 @@ export default async function Page({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PublicRunOutputs /> <PublicRunOutputs preview={sharedDeployment.showcase_media} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -0,0 +1,9 @@
import { SharePageSettings } from "@/components/SharePageSettings";
export default async function Page({
params,
}: {
params: { share_id: string };
}) {
return <SharePageSettings deployment_id={params.share_id} />;
}
+9
View File
@@ -0,0 +1,9 @@
"use client";
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
import { usePathname } from "next/navigation";
export default function Loading() {
const pathName = usePathname();
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
}
+35
View File
@@ -0,0 +1,35 @@
import { setInitialUserData } from "../../../lib/setInitialUserData";
import { auth } from "@clerk/nextjs";
import { clerkClient } from "@clerk/nextjs/server";
import { CheckpointList } from "@/components/CheckpointList"
import { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
export default function Page() {
return <CheckpointListServer />;
}
async function CheckpointListServer() {
const { userId } = auth();
if (!userId) {
return <div>No auth</div>;
}
const user = await clerkClient.users.getUser(userId);
if (!user) {
await setInitialUserData(userId);
}
const checkpoints = await getAllUserCheckpoints()
if (!checkpoints) {
return <div>No checkpoints found</div>;
}
return (
<div className="w-full">
<CheckpointList data={checkpoints}/>
</div>
);
}
@@ -17,7 +17,7 @@ export default async function Page({
<CardHeader className="relative"> <CardHeader className="relative">
<CardTitle>Run</CardTitle> <CardTitle>Run</CardTitle>
<div className="absolute right-6 top-6"> <div className="absolute right-6 top-6">
<RouteRefresher interval={5000} /> <RouteRefresher interval={5000} autoRefresh={false} />
</div> </div>
</CardHeader> </CardHeader>
+10
View File
@@ -9,6 +9,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useAuth, useClerk } from "@clerk/nextjs";
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -49,7 +50,9 @@ export function ButtonActionMenu(props: {
action: () => Promise<any>; action: () => Promise<any>;
}[]; }[];
}) { }) {
const user = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const clerk = useClerk();
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -64,6 +67,13 @@ export function ButtonActionMenu(props: {
<DropdownMenuItem <DropdownMenuItem
key={action.title} key={action.title}
onClick={async () => { onClick={async () => {
if (!user.isSignedIn) {
clerk.openSignIn({
redirectUrl: window.location.href,
});
return;
}
setIsLoading(true); setIsLoading(true);
await callServerPromise(action.action()); await callServerPromise(action.action());
setIsLoading(false); setIsLoading(false);
+315
View File
@@ -0,0 +1,315 @@
"use client";
import { getRelativeTime } from "../lib/getRelativeTime";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { InsertModal, UpdateModal } from "./InsertModal";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
import { addCivitaiCheckpoint } from "@/server/curdCheckpoint";
import { addCivitaiCheckpointSchema } from "@/server/addCheckpointSchema";
export type CheckpointItemList = NonNullable<
Awaited<ReturnType<typeof getAllUserCheckpoints>>
>[0];
export const columns: ColumnDef<CheckpointItemList>[] = [
{
accessorKey: "id",
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
const checkpoint = row.original;
return (
<a
className="hover:underline flex gap-2"
href={`/storage/${checkpoint.id}`} // TODO
>
<span className="truncate max-w-[200px]">{row.original.model_name}</span>
<Badge variant="default">{}</Badge>
{checkpoint.is_public
? <Badge variant="success">Public</Badge>
: <Badge variant="teal">Private</Badge>}
</a>
);
},
},
{
accessorKey: "creator",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Creator
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
// return <Badge variant="cyan">{row?.original?.user?.name ? row.original.user.name : "Public"}</Badge>;
},
},
{
accessorKey: "date",
sortingFn: "datetime",
enableSorting: true,
header: ({ column }) => {
return (
<button
className="w-full flex items-center justify-end hover:underline truncate"
// variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Update Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => (
<div className="w-full capitalize text-right truncate">
{getRelativeTime(row.original.updated_at)}
</div>
),
},
// {
// id: "actions",
// enableHiding: false,
// cell: ({ row }) => {
// const checkpoint = row.original;
//
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
// <DropdownMenuItem
// className="text-destructive"
// onClick={() => {
// deleteWorkflow(checkpoint.id);
// }}
// >
// Delete Workflow
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
// },
];
export function CheckpointList({ data }: { data: CheckpointItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] = React.useState<
VisibilityState
>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="grid grid-rows-[auto,1fr,auto] h-full">
<div className="flex flex-row w-full items-center py-4">
<Input
placeholder="Filter workflows..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className="ml-auto flex gap-2">
<InsertModal
dialogClassName="sm:max-w-[600px]"
disabled={
false
// TODO: limitations based on plan
}
tooltip={"Add models using their civitai url!"}
title="Civitai Checkpoint"
description="Pick a model from civitai"
serverAction={addCivitaiCheckpoint}
formSchema={addCivitaiCheckpointSchema}
fieldConfig={{
civitai_url: {
fieldType: "fallback",
// fieldType: "fallback",
inputProps: { required: true },
description: (
<>
Pick a checkpoint from{" "}
<a
href="https://www.civitai.com/models"
target="_blank"
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
>
civitai.com
</a>{" "}
and place it's url here
</>
),
},
}}
/>
</div>
</div>
<ScrollArea className="h-full w-full rounded-md border">
<Table>
<TableHeader className="bg-background top-0 sticky">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length
? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)
: (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
<div className="flex flex-row items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
+81 -123
View File
@@ -1,6 +1,5 @@
import { ButtonAction } from "@/components/ButtonActionLoader"; import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
import { CodeBlock } from "@/components/CodeBlock"; import { CodeBlock } from "@/components/CodeBlock";
import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -10,15 +9,10 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { TableCell, TableRow } from "@/components/ui/table"; import { TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow"; import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { getRelativeTime } from "@/lib/getRelativeTime";
import { removePublicShareDeployment } from "@/server/curdDeploments";
import type { findAllDeployments } from "@/server/findAllRuns"; import type { findAllDeployments } from "@/server/findAllRuns";
import { ExternalLink } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
const curlTemplate = ` const curlTemplate = `
curl --request POST \ curl --request POST \
@@ -90,32 +84,22 @@ const run = await client.getRun(run_id);
export function DeploymentDisplay({ export function DeploymentDisplay({
deployment, deployment,
domain,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
domain: string;
}) { }) {
const headersList = headers();
const host = headersList.get("host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
const domain = `${protocol}://${host}`;
const workflowInput = getInputsFromWorkflow(deployment.version); const workflowInput = getInputsFromWorkflow(deployment.version);
if (deployment.environment == "public-share") {
return <SharePageDeploymentRow deployment={deployment} />;
}
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer"> <DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow> <TableRow>
<TableCell className="capitalize truncate"> <DeploymentRow deployment={deployment} />
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow> </TableRow>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
@@ -126,105 +110,79 @@ export function DeploymentDisplay({
<DialogDescription>Code for your deployment client</DialogDescription> <DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="max-h-[600px] pr-4"> <ScrollArea className="max-h-[600px] pr-4">
{deployment.environment !== "public-share" ? ( <Tabs defaultValue="client" className="w-full gap-2 text-sm">
<Tabs defaultValue="client" className="w-full gap-2 text-sm"> <TabsList className="grid w-fit grid-cols-3 mb-2">
<TabsList className="grid w-fit grid-cols-3 mb-2"> <TabsTrigger value="client">Server Client</TabsTrigger>
<TabsTrigger value="client">Server Client</TabsTrigger> <TabsTrigger value="js">NodeJS Fetch</TabsTrigger>
<TabsTrigger value="js">NodeJS Fetch</TabsTrigger> <TabsTrigger value="curl">CURL</TabsTrigger>
<TabsTrigger value="curl">CURL</TabsTrigger> </TabsList>
</TabsList> <TabsContent className="flex flex-col gap-2 !mt-0" value="client">
<TabsContent className="flex flex-col gap-2 !mt-0" value="client"> <div>
<div> Copy and paste the ComfyDeployClient form&nbsp;
Copy and paste the ComfyDeployClient form&nbsp; <a
<a href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts" className="text-blue-500 hover:underline"
className="text-blue-500 hover:underline" target="_blank"
target="_blank"
>
here
</a>
</div>
<CodeBlock
lang="js"
code={formatCode(
domain == "https://www.comfydeploy.com"
? jsClientSetupTemplateHostedVersion
: jsClientSetupTemplate,
deployment,
domain,
workflowInput
)}
/>
Create a run via deployment id
<CodeBlock
lang="js"
code={formatCode(
workflowInput && workflowInput.length > 0
? jsClientCreateRunTemplate
: jsClientCreateRunNoInputsTemplate,
deployment,
domain,
workflowInput
)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(
clientTemplate_checkStatus,
deployment,
domain
)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-0" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(
jsTemplate,
deployment,
domain,
workflowInput
)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(
curlTemplate_checkStatus,
deployment,
domain
)}
/>
</TabsContent>
</Tabs>
) : (
<div className="w-full justify-end flex gap-2 py-1">
<Button asChild className="gap-2" variant="outline" type="submit">
<ButtonAction
action={removePublicShareDeployment.bind(null, deployment.id)}
> >
Remove here
</ButtonAction> </a>
</Button> </div>
<Button asChild className="gap-2"> <CodeBlock
<Link href={`/share/${deployment.id}`} target="_blank"> lang="js"
View Share Page <ExternalLink size={14} /> code={formatCode(
</Link> domain == "https://www.comfydeploy.com"
</Button> ? jsClientSetupTemplateHostedVersion
</div> : jsClientSetupTemplate,
)} deployment,
domain,
workflowInput
)}
/>
Create a run via deployment id
<CodeBlock
lang="js"
code={formatCode(
workflowInput && workflowInput.length > 0
? jsClientCreateRunTemplate
: jsClientCreateRunNoInputsTemplate,
deployment,
domain,
workflowInput
)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(
clientTemplate_checkStatus,
deployment,
domain
)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-0" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(jsTemplate, deployment, domain, workflowInput)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
</Tabs>
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { TableCell, TableRow } from "@/components/ui/table";
import { getRelativeTime } from "@/lib/getRelativeTime";
import type { findAllDeployments } from "@/server/findAllRuns";
import { useRouter } from "next/navigation";
export function SharePageDeploymentRow({
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
const router = useRouter();
return (
<TableRow
className="appearance-none hover:cursor-pointer"
onClick={() => {
if (deployment.environment == "public-share") {
router.push(`/share/${deployment.id}/settings`);
}
}}
>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
);
}
export function DeploymentRow({
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
return (
<>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</>
);
}
+28 -11
View File
@@ -11,6 +11,7 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Tooltip, Tooltip,
@@ -106,12 +107,14 @@ export function UpdateModal<
Y extends UnknownKeysParam, Y extends UnknownKeysParam,
Z extends ZodObject<K, Y> Z extends ZodObject<K, Y>
>(props: { >(props: {
open: boolean; open?: boolean;
setOpen: (open: boolean) => void; setOpen?: (open: boolean) => void;
title: string; title: string;
description: string; description: string;
dialogClassName?: string; dialogClassName?: string;
data: z.infer<Z>; data: z.infer<Z> & {
id: string;
};
serverAction: ( serverAction: (
data: z.infer<Z> & { data: z.infer<Z> & {
id: string; id: string;
@@ -119,8 +122,13 @@ export function UpdateModal<
) => Promise<unknown>; ) => Promise<unknown>;
formSchema: Z; formSchema: Z;
fieldConfig?: FieldConfig<z.infer<Z>>; fieldConfig?: FieldConfig<z.infer<Z>>;
trigger?: React.ReactNode;
extraButtons?: React.ReactNode;
}) { }) {
// const [open, setOpen] = React.useState(false); const [_open, _setOpen] = React.useState(false);
const open = props.open ?? _open;
const setOpen = props.setOpen ?? _setOpen;
const [values, setValues] = useState<Partial<z.infer<Z>>>({}); const [values, setValues] = useState<Partial<z.infer<Z>>>({});
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
@@ -129,10 +137,18 @@ export function UpdateModal<
}, [props.data]); }, [props.data]);
return ( return (
<Dialog open={props.open} onOpenChange={props.setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{/* <DialogTrigger asChild> {props.trigger ?? (
<DropdownMenuItem>{props.title}</DropdownMenuItem> <DialogTrigger
</DialogTrigger> */} className="appearance-none hover:cursor-pointer"
asChild
onClick={() => {
setOpen(true);
}}
>
{props.trigger}
</DialogTrigger>
)}
<DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}> <DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
<DialogHeader> <DialogHeader>
<DialogTitle>{props.title}</DialogTitle> <DialogTitle>{props.title}</DialogTitle>
@@ -152,13 +168,14 @@ export function UpdateModal<
}) })
); );
setIsLoading(false); setIsLoading(false);
props.setOpen(false); setOpen(false);
}} }}
> >
<div className="flex justify-end"> <div className="flex justify-end flex-wrap gap-2">
{props.extraButtons}
<AutoFormSubmit> <AutoFormSubmit>
Save Changes Save Changes
<span className="ml-2">{isLoading && <LoadingIcon />}</span> {isLoading && <LoadingIcon />}
</AutoFormSubmit> </AutoFormSubmit>
</div> </div>
</AutoForm> </AutoForm>
+1 -1
View File
@@ -69,7 +69,7 @@ export default async function Main() {
</Section.Announcement> </Section.Announcement>
<Section.Title className="text-left"> <Section.Title className="text-left">
<span className="text-6xl md:text-7xl pb-2 inline-flex animate-background-shine bg-[linear-gradient(110deg,#1e293b,45%,#939393,55%,#1e293b)] bg-[length:250%_100%] bg-clip-text text-transparent"> <span className="text-5xl sm:text-6xl md:text-7xl pb-2 inline-flex animate-background-shine bg-[linear-gradient(110deg,#1e293b,45%,#939393,55%,#1e293b)] bg-[length:250%_100%] bg-clip-text text-transparent">
{meta.tagline} {meta.tagline}
</span> </span>
</Section.Title> </Section.Title>
+6 -2
View File
@@ -34,6 +34,10 @@ export function NavbarMenu({ className }: { className?: string }) {
name: "API Keys", name: "API Keys",
path: "/api-keys", path: "/api-keys",
}, },
{
name: "Storage",
path: "/storage",
},
]; ];
return ( return (
@@ -42,9 +46,9 @@ export function NavbarMenu({ className }: { className?: string }) {
{isDesktop && ( {isDesktop && (
<Tabs <Tabs
defaultValue={pathname} defaultValue={pathname}
className="w-[300px] flex pointer-events-auto" className="w-[400px] flex pointer-events-auto"
> >
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-4">
{pages.map((page) => ( {pages.map((page) => (
<TabsTrigger <TabsTrigger
key={page.name} key={page.name}
+52
View File
@@ -0,0 +1,52 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
} from "@/components/ui/carousel";
import * as React from "react";
export function OutputPreview() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [count, setCount] = React.useState(0);
React.useEffect(() => {
if (!api) {
return;
}
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
api.on("select", () => {
setCurrent(api.selectedScrollSnap() + 1);
});
}, [api]);
return (
<div>
<Carousel setApi={setApi} className="w-full max-w-xs">
<CarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<CarouselItem key={index}>
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div className="py-2 text-center text-sm text-muted-foreground">
Slide {current} of {count}
</div>
</div>
);
}
+27 -3
View File
@@ -1,14 +1,21 @@
"use client"; "use client";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import { Button } from "@/components/ui/button";
import { RefreshCcw } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useTransition } from "react"; import { useEffect, useTransition } from "react";
export function RouteRefresher(props: { interval: number }) { export function RouteRefresher(props: {
interval: number;
autoRefresh: boolean;
}) {
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (!props.autoRefresh) return;
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
const refresh = () => { const refresh = () => {
@@ -35,7 +42,24 @@ export function RouteRefresher(props: { interval: number }) {
clearTimeout(timeout); clearTimeout(timeout);
window.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("visibilitychange", handleVisibilityChange);
}; };
}, [props.interval, router]); }, [props.interval, router, props.autoRefresh]);
return <div>{isPending && <LoadingIcon />}</div>; return (
<div>
{isPending && <LoadingIcon />}
{!isPending && !props.autoRefresh && (
<Button
className="p-0 h-min"
variant="ghost"
onClick={() => {
startTransition(() => {
router.refresh();
});
}}
>
<RefreshCcw size={14} />
</Button>
)}
</div>
);
} }
+7 -2
View File
@@ -11,7 +11,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { TableCell, TableRow } from "@/components/ui/table"; import { TableCell, TableRow } from "@/components/ui/table";
import { getRelativeTime } from "@/lib/getRelativeTime"; import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
import { type findAllRuns } from "@/server/findAllRuns"; import { type findAllRuns } from "@/server/findAllRuns";
import { Suspense } from "react"; import { Suspense } from "react";
@@ -33,7 +33,12 @@ export async function RunDisplay({
</TableCell> </TableCell>
<TableCell>{run.version?.version}</TableCell> <TableCell>{run.version?.version}</TableCell>
<TableCell> <TableCell>
<Badge variant="outline">{run.origin}</Badge> <Badge variant="outline" className="truncate">
{run.origin}
</Badge>
</TableCell>
<TableCell className="truncate">
{getDuration(run.duration)}
</TableCell> </TableCell>
<LiveStatus run={run} /> <LiveStatus run={run} />
</TableRow> </TableRow>
-3
View File
@@ -40,13 +40,10 @@ export function RunWorkflowInline({
} = publicRunStore(); } = publicRunStore();
const runWorkflow = async () => { const runWorkflow = async () => {
console.log();
if (!user.isSignedIn) { if (!user.isSignedIn) {
clerk.openSignIn({ clerk.openSignIn({
redirectUrl: window.location.href, redirectUrl: window.location.href,
}); });
console.log("hi");
return; return;
} }
console.log(values); console.log(values);
+14 -6
View File
@@ -14,6 +14,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { parseAsInteger } from "next-usequerystate"; import { parseAsInteger } from "next-usequerystate";
import { headers } from "next/headers";
const itemPerPage = 6; const itemPerPage = 6;
const pageParser = parseAsInteger.withDefault(1); const pageParser = parseAsInteger.withDefault(1);
@@ -40,11 +41,12 @@ export async function RunsTable(props: {
)} )}
<TableHeader className="bg-background top-0 sticky"> <TableHeader className="bg-background top-0 sticky">
<TableRow> <TableRow>
<TableHead className="w-[100px]">Number</TableHead> <TableHead className="truncate">Number</TableHead>
<TableHead className="">Machine</TableHead> <TableHead className="truncate">Machine</TableHead>
<TableHead className="">Time</TableHead> <TableHead className="truncate">Time</TableHead>
<TableHead className="w-[100px]">Version</TableHead> <TableHead className="truncate">Version</TableHead>
<TableHead className="truncate">Origin</TableHead> <TableHead className="truncate">Origin</TableHead>
<TableHead className="truncate">Duration</TableHead>
<TableHead className="truncate">Live Status</TableHead> <TableHead className="truncate">Live Status</TableHead>
<TableHead className="text-right">Status</TableHead> <TableHead className="text-right">Status</TableHead>
</TableRow> </TableRow>
@@ -69,8 +71,14 @@ export async function RunsTable(props: {
export async function DeploymentsTable(props: { workflow_id: string }) { export async function DeploymentsTable(props: { workflow_id: string }) {
const allRuns = await findAllDeployments(props.workflow_id); const allRuns = await findAllDeployments(props.workflow_id);
const headersList = headers();
const host = headersList.get("host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
const domain = `${protocol}://${host}`;
return ( return (
<div className="overflow-auto h-fit w-full"> <div className="overflow-auto h-fit w-full">
<Table className=""> <Table className="">
<TableCaption>A list of your deployments</TableCaption> <TableCaption>A list of your deployments</TableCaption>
<TableHeader className="bg-background top-0 sticky"> <TableHeader className="bg-background top-0 sticky">
@@ -83,7 +91,7 @@ export async function DeploymentsTable(props: { workflow_id: string }) {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{allRuns.map((run) => ( {allRuns.map((run) => (
<DeploymentDisplay deployment={run} key={run.id} /> <DeploymentDisplay deployment={run} key={run.id} domain={domain} />
))} ))}
</TableBody> </TableBody>
</Table> </Table>
+79 -75
View File
@@ -1,33 +1,37 @@
import { Button, buttonVariants } from '@/components/ui/button';
type ButtonProps = React.ComponentProps<typeof Button>;
type LinkProps = React.ComponentProps<typeof Link>;
import { Card as BaseCard } from '@/components/ui/card';
type CardProps = React.ComponentProps<typeof BaseCard>;
import { Tabs, TabsTrigger as Tab, TabsList } from '@/components/ui/tabs';
type TabsProps = React.ComponentProps<typeof Tabs>;
import { import {
Accordion, Accordion,
AccordionItem, AccordionItem,
AccordionContent, AccordionContent,
AccordionTrigger, AccordionTrigger,
} from '@/components/ui/accordion'; } from "@/components/ui/accordion";
type AccordionProps = React.ComponentProps<typeof Accordion>; import { Badge as Chip } from "@/components/ui/badge";
import { Badge as Chip } from '@/components/ui/badge'; import { Button, buttonVariants } from "@/components/ui/button";
type ChipProps = React.ComponentProps<typeof Chip>; import { Card as BaseCard } from "@/components/ui/card";
import { Tabs, TabsTrigger as Tab, TabsList } from "@/components/ui/tabs";
import Link from 'next/link'; // import { PiCheckCircleDuotone } from 'react-icons/pi';
import { cn } from "@/lib/utils";
import { ChevronRight as MdChevronRight } from "lucide-react";
import { CheckCircle as PiCheckCircleDuotone } from "lucide-react";
import Link from "next/link";
import type { import type {
HTMLAttributeAnchorTarget, HTMLAttributeAnchorTarget,
HTMLAttributes, HTMLAttributes,
ReactNode, ReactNode,
} from 'react'; } from "react";
import { twMerge } from 'tailwind-merge';
import { ChevronRight as MdChevronRight} from 'lucide-react'
// import { MdChevronRight } from 'react-icons/md'; // import { MdChevronRight } from 'react-icons/md';
import React from 'react'; import React from "react";
import { CheckCircle as PiCheckCircleDuotone } from 'lucide-react' import { twMerge } from "tailwind-merge";
// import { PiCheckCircleDuotone } from 'react-icons/pi';
import { cn } from '@/lib/utils'; type ButtonProps = React.ComponentProps<typeof Button>;
type LinkProps = React.ComponentProps<typeof Link>;
type CardProps = React.ComponentProps<typeof BaseCard>;
type TabsProps = React.ComponentProps<typeof Tabs>;
type AccordionProps = React.ComponentProps<typeof Accordion>;
type ChipProps = React.ComponentProps<typeof Chip>;
function Section({ function Section({
className, className,
@@ -41,8 +45,8 @@ function Section({
return ( return (
<section <section
className={twMerge( className={twMerge(
'flex min-h-[400px] w-full max-w-6xl flex-col justify-center gap-2 rounded-lg px-10 py-10 md:px-20', "flex min-h-[400px] w-full max-w-6xl flex-col justify-center gap-2 rounded-lg px-2 sm:px-10 py-10 md:px-20",
className, className
)} )}
{...props} {...props}
> >
@@ -64,12 +68,12 @@ function Title({
<h1 <h1
{...props} {...props}
className={twMerge( className={twMerge(
'text-center text-4xl font-bold md:text-6xl', "text-center text-4xl font-bold md:text-6xl",
className, className
)} )}
style={{ style={{
// @ts-ignore // @ts-ignore
textWrap: 'balance', textWrap: "balance",
}} }}
> >
{children} {children}
@@ -86,12 +90,12 @@ function Subtitle({
<h2 <h2
{...props} {...props}
className={twMerge( className={twMerge(
'text text-center overflow-hidden text-ellipsis text-xl', "text text-center overflow-hidden text-ellipsis text-xl",
className, className
)} )}
style={{ style={{
// @ts-ignore // @ts-ignore
textWrap: 'balance', textWrap: "balance",
}} }}
> >
{children} {children}
@@ -103,7 +107,7 @@ function Announcement({
className, className,
children, children,
href, href,
target = '_blank', target = "_blank",
...props ...props
}: ChipProps & { }: ChipProps & {
href?: string; //string | UrlObject; href?: string; //string | UrlObject;
@@ -112,8 +116,8 @@ function Announcement({
return ( return (
<Chip <Chip
className={twMerge( className={twMerge(
'w-fit group bg-foreground-50 text-center transition-colors hover:bg-gray-200', "w-fit group bg-foreground-50 text-center transition-colors hover:bg-gray-200",
className, className
)} )}
variant="outline" variant="outline"
// href={href} // href={href}
@@ -127,13 +131,13 @@ function Announcement({
// } // }
style={{ style={{
// @ts-ignore // @ts-ignore
textWrap: 'balance', textWrap: "balance",
}} }}
{...props} {...props}
> >
<a href={href} target={target}> <a href={href} target={target}>
{children} {children}
</a>{' '} </a>{" "}
<MdChevronRight <MdChevronRight
size={20} size={20}
className="pr-1 transition-transform group-hover:translate-x-[2px]" className="pr-1 transition-transform group-hover:translate-x-[2px]"
@@ -143,14 +147,14 @@ function Announcement({
} }
type ActionProps = ButtonProps & { type ActionProps = ButtonProps & {
be: 'button'; be: "button";
hideArrow?: boolean; hideArrow?: boolean;
}; };
type ActionLinkProps = LinkProps & { type ActionLinkProps = LinkProps & {
be?: 'a'; be?: "a";
hideArrow?: boolean; hideArrow?: boolean;
variant?: ButtonProps['variant']; variant?: ButtonProps["variant"];
}; };
function PrimaryAction({ function PrimaryAction({
@@ -160,15 +164,15 @@ function PrimaryAction({
hideArrow, hideArrow,
...props ...props
}: ActionLinkProps | ActionProps) { }: ActionLinkProps | ActionProps) {
if (props.be === 'button') { if (props.be === "button") {
return ( return (
<Button <Button
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: variant, variant: variant,
}), }),
'group', "group",
className, className
)} )}
{...props} {...props}
> >
@@ -186,8 +190,8 @@ function PrimaryAction({
buttonVariants({ buttonVariants({
variant: variant, variant: variant,
}), }),
'group', "group",
className, className
)} )}
{...props} {...props}
> >
@@ -206,17 +210,17 @@ function SecondaryAction({
hideArrow, hideArrow,
...props ...props
}: ActionLinkProps | ActionProps) { }: ActionLinkProps | ActionProps) {
if (props.be === 'button') { if (props.be === "button") {
return ( return (
<Button <Button
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: variant, variant: variant,
}), }),
'group', "group",
className, className
)} )}
variant={'ghost'} variant="ghost"
{...props} {...props}
> >
{children} {children}
@@ -231,10 +235,10 @@ function SecondaryAction({
<Link <Link
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: 'ghost', variant: "ghost",
}), }),
'group', "group",
className, className
)} )}
{...props} {...props}
> >
@@ -249,31 +253,31 @@ function PricingCard({
className, className,
children, children,
...props ...props
}: Omit<CardProps, 'children'> & { }: Omit<CardProps, "children"> & {
children: children:
| ReactNode | ReactNode
| ReactNode[] | ReactNode[]
| ((pricingType: PricingType) => ReactNode | ReactNode[]); | ((pricingType: PricingType) => ReactNode | ReactNode[]);
}) { }) {
// const { pricingType } = usePricingContext(); // const { pricingType } = usePricingContext();
if (typeof children === 'function') if (typeof children === "function")
children = (children('month') as React.ReactElement).props.children as children = (children("month") as React.ReactElement).props.children as
| ReactNode | ReactNode
| ReactNode[]; | ReactNode[];
// extract the title and subtitle from the children // extract the title and subtitle from the children
// const cardTitleStyles = // const cardTitleStyles =
const title = getChildComponent(children, Title, { const title = getChildComponent(children, Title, {
className: 'text-2xl md:text-2xl text-start font-bold', className: "text-2xl md:text-2xl text-start font-bold",
}); });
const subTitle = getChildComponent(children, Subtitle, { const subTitle = getChildComponent(children, Subtitle, {
className: 'text-md text-start text-foreground-500 mt-4', className: "text-md text-start text-foreground-500 mt-4",
}); });
const priceTags = getChildComponents(children, PriceTag, { const priceTags = getChildComponents(children, PriceTag, {
className: 'text-4xl font-bold', className: "text-4xl font-bold",
}); });
const primaryAction = getChildComponent(children, PrimaryAction, { const primaryAction = getChildComponent(children, PrimaryAction, {
className: 'w-full', className: "w-full",
}); });
return ( return (
@@ -281,8 +285,8 @@ function PricingCard({
// shadow="sm" // shadow="sm"
{...props} {...props}
className={twMerge( className={twMerge(
'flex flex-col min-h-[400px] w-full max-w-full items-start justify-between gap-2 p-8 text-sm', "flex flex-col min-h-[400px] w-full max-w-full items-start justify-between gap-2 p-8 text-sm",
className, className
)} )}
> >
<div> <div>
@@ -320,7 +324,7 @@ function PricingCard({
// setPricingType: (pricingType: PricingType) => {}, // setPricingType: (pricingType: PricingType) => {},
// }); // });
const PricingTypeValue = ['month', 'year'] as const; const PricingTypeValue = ["month", "year"] as const;
export type PricingType = (typeof PricingTypeValue)[number]; export type PricingType = (typeof PricingTypeValue)[number];
// // an helper function to useContext // // an helper function to useContext
@@ -350,7 +354,7 @@ function PricingOption({ className, ...props }: TabsProps) {
return ( return (
<Tabs <Tabs
className={twMerge('w-fit', className)} className={twMerge("w-fit", className)}
defaultValue="month" defaultValue="month"
aria-label="Pricing Options" aria-label="Pricing Options"
{...props} {...props}
@@ -384,10 +388,10 @@ function PriceTag({
pricingType, pricingType,
...props ...props
}: HTMLAttributes<HTMLHeadingElement> & { }: HTMLAttributes<HTMLHeadingElement> & {
pricingType?: 'month' | 'year' | string; pricingType?: "month" | "year" | string;
}) { }) {
// const { pricingType: currentPricingType } = usePricingContext(); // const { pricingType: currentPricingType } = usePricingContext();
let currentPricingType = 'month'; const currentPricingType = "month";
if (pricingType != undefined && currentPricingType !== pricingType) if (pricingType != undefined && currentPricingType !== pricingType)
return <></>; return <></>;
@@ -399,10 +403,10 @@ function Card({ className, children, ...props }: CardProps) {
// extract the title and subtitle from the children // extract the title and subtitle from the children
// const cardTitleStyles = // const cardTitleStyles =
const title = getChildComponent(children, Title, { const title = getChildComponent(children, Title, {
className: 'text-2xl md:text-2xl font-normal text-center', className: "text-2xl md:text-2xl font-normal text-center",
}); });
const subTitle = getChildComponent(children, Subtitle, { const subTitle = getChildComponent(children, Subtitle, {
className: 'text-md text-center', className: "text-md text-center",
}); });
const image = getChildComponent(children, ImageArea); const image = getChildComponent(children, ImageArea);
@@ -411,8 +415,8 @@ function Card({ className, children, ...props }: CardProps) {
// shadow="sm" // shadow="sm"
{...props} {...props}
className={twMerge( className={twMerge(
'flex min-h-[280px] w-full max-w-full items-center justify-center gap-2 p-4 text-sm flex-col', "flex min-h-[280px] w-full max-w-full items-center justify-center gap-2 p-4 text-sm flex-col",
className, className
)} )}
> >
{image} {image}
@@ -431,7 +435,7 @@ function ImageArea({
return ( return (
<div <div
{...props} {...props}
className={twMerge('aspect-square w-14 bg-foreground-300', className)} className={twMerge("aspect-square w-14 bg-foreground-300", className)}
> >
{children} {children}
</div> </div>
@@ -442,11 +446,11 @@ function ImageArea({
function getChildComponent<T extends (...args: any[]) => React.JSX.Element>( function getChildComponent<T extends (...args: any[]) => React.JSX.Element>(
children: React.ReactNode | React.ReactNode[], children: React.ReactNode | React.ReactNode[],
type: T, type: T,
propsOverride?: Partial<Parameters<T>[0]>, propsOverride?: Partial<Parameters<T>[0]>
) { ) {
const childrenArr = React.Children.toArray(children); const childrenArr = React.Children.toArray(children);
let child = childrenArr.find( let child = childrenArr.find(
(child) => React.isValidElement(child) && child.type === type, (child) => React.isValidElement(child) && child.type === type
) as React.ReactElement< ) as React.ReactElement<
Parameters<T>[0], Parameters<T>[0],
string | React.JSXElementConstructor<any> string | React.JSXElementConstructor<any>
@@ -466,12 +470,12 @@ function getChildComponent<T extends (...args: any[]) => React.JSX.Element>(
function getChildComponents<T extends (...args: any[]) => React.JSX.Element>( function getChildComponents<T extends (...args: any[]) => React.JSX.Element>(
children: React.ReactNode | React.ReactNode[], children: React.ReactNode | React.ReactNode[],
type: T, type: T,
propsOverride?: Partial<Parameters<T>[0]>, propsOverride?: Partial<Parameters<T>[0]>
) { ) {
const childrenArr = React.Children.toArray(children); const childrenArr = React.Children.toArray(children);
let child = ( const child = (
childrenArr.filter( childrenArr.filter(
(child) => React.isValidElement(child) && child.type === type, (child) => React.isValidElement(child) && child.type === type
) as React.ReactElement< ) as React.ReactElement<
Parameters<T>[0], Parameters<T>[0],
string | React.JSXElementConstructor<any> string | React.JSXElementConstructor<any>
@@ -492,10 +496,10 @@ function getChildComponents<T extends (...args: any[]) => React.JSX.Element>(
function removeFromChildren( function removeFromChildren(
children: React.ReactNode | React.ReactNode[], children: React.ReactNode | React.ReactNode[],
types: any[], types: any[]
): React.ReactNode[] { ): React.ReactNode[] {
return React.Children.toArray(children).filter( return React.Children.toArray(children).filter(
(child) => React.isValidElement(child) && !types.includes(child.type), (child) => React.isValidElement(child) && !types.includes(child.type)
); );
} }
@@ -508,11 +512,11 @@ function FAQItem({
...props ...props
}: { }: {
children: React.ReactNode | React.ReactNode[]; children: React.ReactNode | React.ReactNode[];
'aria-label': string; "aria-label": string;
title: string; title: string;
}): JSX.Element { }): JSX.Element {
return ( return (
<AccordionItem value={props['aria-label']}> <AccordionItem value={props["aria-label"]}>
<AccordionTrigger>{props.title}</AccordionTrigger> <AccordionTrigger>{props.title}</AccordionTrigger>
<AccordionContent>{children}</AccordionContent> <AccordionContent>{children}</AccordionContent>
</AccordionItem> </AccordionItem>
+90
View File
@@ -0,0 +1,90 @@
"use client";
import { useServerActionData } from "./useServerActionData";
import { ButtonAction } from "@/components/ButtonActionLoader";
import { UpdateModal } from "@/components/InsertModal";
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
import { Button } from "@/components/ui/button";
import { publicShareDeployment } from "@/db/schema";
import {
findUserShareDeployment,
removePublicShareDeployment,
updateSharePageInfo,
} from "@/server/curdDeploments";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
export function SharePageSettings({
deployment_id,
}: {
deployment_id: string;
}) {
const {
data: deployment,
pending,
started,
} = useServerActionData(findUserShareDeployment, deployment_id);
const [_open, _setOpen] = useState(false);
const router = useRouter();
if (pending) return <LoadingPageWrapper className="h-full" tag="settings" />;
if (!deployment && started && !pending)
return (
<div className="h-full w-full flex items-center justify-center">
<p>Settings page not found.</p>
</div>
);
if (!deployment) return null;
return (
<>
<UpdateModal
dialogClassName="sm:max-w-[600px]"
open={true}
setOpen={() => {
router.back();
}}
extraButtons={
<>
<Button
asChild
className="gap-2 truncate"
variant="outline"
type="button"
>
<ButtonAction
action={removePublicShareDeployment.bind(null, deployment.id)}
>
Remove
</ButtonAction>
</Button>
<Button asChild className="gap-2 truncate" type="button">
<Link href={`/share/${deployment.id}`} target="_blank">
View Share Page <ExternalLink size={14} />
</Link>
</Button>
</>
}
data={{
id: deployment.id,
description: deployment.description,
showcase_media: deployment.showcase_media ?? [],
}}
title="Share Page"
description="Edit share page details."
serverAction={updateSharePageInfo}
formSchema={publicShareDeployment}
fieldConfig={{
description: {
fieldType: "textarea",
},
}}
/>
</>
);
}
+13 -2
View File
@@ -39,7 +39,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { workflowAPINodeType } from "@/db/schema"; import type { showcaseMediaNullable, workflowAPINodeType } from "@/db/schema";
import { checkStatus, createRun } from "@/server/createRun"; import { checkStatus, createRun } from "@/server/createRun";
import { createDeployments } from "@/server/curdDeploments"; import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine"; import type { getMachines } from "@/server/curdMachine";
@@ -154,7 +154,9 @@ export const publicRunStore = create<PublicRunStore>((set) => ({
setStatus: (status) => set({ status }), setStatus: (status) => set({ status }),
})); }));
export function PublicRunOutputs() { export function PublicRunOutputs(props: {
preview: z.infer<typeof showcaseMediaNullable>;
}) {
const { image, loading, runId, status, setStatus, setImage, setLoading } = const { image, loading, runId, status, setStatus, setImage, setLoading } =
publicRunStore(); publicRunStore();
@@ -176,6 +178,15 @@ export function PublicRunOutputs() {
return ( return (
<div className="border border-gray-200 w-full square h-[400px] rounded-lg relative"> <div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
{!loading && !image && props.preview && props.preview.length > 0 && (
<>
<img
className="w-full h-full object-contain"
src={props.preview[0]?.url}
alt="Generated image"
/>
</>
)}
{!loading && image && ( {!loading && image && (
<img <img
className="w-full h-full object-contain" className="w-full h-full object-contain"
+104 -102
View File
@@ -42,105 +42,105 @@ const Model = z.object({
url: z.string(), url: z.string(),
}); });
export const CivitalModelSchema = z.object({ export const CivitaiModel = z.object({
items: z.array( id: z.number(),
name: z.string(),
description: z.string(),
type: z.string(),
// poi: z.boolean(),
// nsfw: z.boolean(),
// allowNoCredit: z.boolean(),
// allowCommercialUse: z.string(),
// allowDerivatives: z.boolean(),
// allowDifferentLicense: z.boolean(),
// stats: z.object({
// downloadCount: z.number(),
// favoriteCount: z.number(),
// commentCount: z.number(),
// ratingCount: z.number(),
// rating: z.number(),
// tippedAmountCount: z.number(),
// }),
creator: z
.object({
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
z.object({ z.object({
id: z.number(), id: z.number(),
modelId: z.number(),
name: z.string(), name: z.string(),
description: z.string(), createdAt: z.string(),
type: z.string(), updatedAt: z.string(),
// poi: z.boolean(), status: z.string(),
// nsfw: z.boolean(), publishedAt: z.string(),
// allowNoCredit: z.boolean(), trainedWords: z.array(z.unknown()),
// allowCommercialUse: z.string(), trainingStatus: z.string().nullable(),
// allowDerivatives: z.boolean(), trainingDetails: z.string().nullable(),
// allowDifferentLicense: z.boolean(), baseModel: z.string(),
// stats: z.object({ baseModelType: z.string().nullable(),
// downloadCount: z.number(), earlyAccessTimeFrame: z.number(),
// favoriteCount: z.number(), description: z.string().nullable(),
// commentCount: z.number(), vaeId: z.number().nullable(),
// ratingCount: z.number(), stats: z.object({
// rating: z.number(), downloadCount: z.number(),
// tippedAmountCount: z.number(), ratingCount: z.number(),
// }), rating: z.number(),
creator: z }),
.object({ files: z.array(
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
z.object({ z.object({
id: z.number(), id: z.number(),
modelId: z.number(), sizeKB: z.number(),
name: z.string(), name: z.string(),
createdAt: z.string(), type: z.string(),
updatedAt: z.string(), // metadata: z.object({
status: z.string(), // fp: z.string().nullable().optional(),
publishedAt: z.string(), // size: z.string().nullable().optional(),
trainedWords: z.array(z.unknown()), // format: z.string().nullable().optional(),
trainingStatus: z.string().nullable(), // }),
trainingDetails: z.string().nullable(), // pickleScanResult: z.string(),
baseModel: z.string(), // pickleScanMessage: z.string(),
baseModelType: z.string().nullable(), // virusScanResult: z.string(),
earlyAccessTimeFrame: z.number(), // virusScanMessage: z.string().nullable(),
description: z.string().nullable(), // scannedAt: z.string(),
vaeId: z.number().nullable(), // hashes: z.object({
stats: z.object({ // AutoV1: z.string().nullable().optional(),
downloadCount: z.number(), // AutoV2: z.string().nullable().optional(),
ratingCount: z.number(), // SHA256: z.string().nullable().optional(),
rating: z.number(), // CRC32: z.string().nullable().optional(),
}), // BLAKE3: z.string().nullable().optional(),
files: z.array( // }),
z.object({
id: z.number(),
sizeKB: z.number(),
name: z.string(),
type: z.string(),
// metadata: z.object({
// fp: z.string().nullable().optional(),
// size: z.string().nullable().optional(),
// format: z.string().nullable().optional(),
// }),
// pickleScanResult: z.string(),
// pickleScanMessage: z.string(),
// virusScanResult: z.string(),
// virusScanMessage: z.string().nullable(),
// scannedAt: z.string(),
// hashes: z.object({
// AutoV1: z.string().nullable().optional(),
// AutoV2: z.string().nullable().optional(),
// SHA256: z.string().nullable().optional(),
// CRC32: z.string().nullable().optional(),
// BLAKE3: z.string().nullable().optional(),
// }),
downloadUrl: z.string(),
// primary: z.boolean().default(false),
})
),
images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}),
meta: z.any(),
})
),
downloadUrl: z.string(), downloadUrl: z.string(),
}) // primary: z.boolean().default(false),
}),
), ),
}) images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}),
meta: z.any(),
}),
),
downloadUrl: z.string(),
}),
), ),
});
export const CivitalModelSchema = z.object({
items: z.array(CivitaiModel),
metadata: z.object({ metadata: z.object({
totalItems: z.number(), totalItems: z.number(),
currentPage: z.number(), currentPage: z.number(),
@@ -197,7 +197,7 @@ function mapType(type: string) {
} }
function mapModelsList( function mapModelsList(
models: z.infer<typeof CivitalModelSchema> models: z.infer<typeof CivitalModelSchema>,
): z.infer<typeof ModelListWrapper> { ): z.infer<typeof ModelListWrapper> {
return { return {
models: models.items.flatMap((item) => { models: models.items.flatMap((item) => {
@@ -241,8 +241,9 @@ function getUrl(search?: string) {
export function CivitaiModelRegistry({ export function CivitaiModelRegistry({
field, field,
}: Pick<AutoFormInputComponentProps, "field">) { }: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] = const [modelList, setModelList] = React.useState<
React.useState<z.infer<typeof ModelListWrapper>>(); z.infer<typeof ModelListWrapper>
>();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
@@ -301,8 +302,9 @@ export function CivitaiModelRegistry({
export function ComfyUIManagerModelRegistry({ export function ComfyUIManagerModelRegistry({
field, field,
}: Pick<AutoFormInputComponentProps, "field">) { }: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] = const [modelList, setModelList] = React.useState<
React.useState<z.infer<typeof ModelListWrapper>>(); z.infer<typeof ModelListWrapper>
>();
React.useEffect(() => { React.useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
@@ -310,7 +312,7 @@ export function ComfyUIManagerModelRegistry({
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json", "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
{ {
signal: controller.signal, signal: controller.signal,
} },
) )
.then((x) => x.json()) .then((x) => x.json())
.then((a) => { .then((a) => {
@@ -353,14 +355,14 @@ export function ModelSelector({
if ( if (
prevSelectedModels.some( prevSelectedModels.some(
(selectedModel) => (selectedModel) =>
selectedModel.url + selectedModel.name === model.url + model.name selectedModel.url + selectedModel.name === model.url + model.name,
) )
) { ) {
field.onChange( field.onChange(
prevSelectedModels.filter( prevSelectedModels.filter(
(selectedModel) => (selectedModel) =>
selectedModel.url + selectedModel.name !== model.url + model.name selectedModel.url + selectedModel.name !== model.url + model.name,
) ),
); );
} else { } else {
field.onChange([...prevSelectedModels, model]); field.onChange([...prevSelectedModels, model]);
@@ -408,10 +410,10 @@ export function ModelSelector({
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
value.some( value.some(
(selectedModel) => selectedModel.url === model.url (selectedModel) => selectedModel.url === model.url,
) )
? "opacity-100" ? "opacity-100"
: "opacity-0" : "opacity-0",
)} )}
/> />
</CommandItem> </CommandItem>
@@ -0,0 +1,89 @@
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { FormControl, FormItem, FormLabel } from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon";
import * as React from "react";
import AutoFormInput from "../ui/auto-form/fields/input";
import { useDebouncedCallback } from "use-debounce";
import { CivitaiModel } from "./ModelPickerView";
import { z } from "zod";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export default function AutoFormCheckpointInput(
props: AutoFormInputComponentProps,
) {
const [loading, setLoading] = React.useState(false);
const [modelRes, setModelRes] = React.useState<
z.infer<typeof CivitaiModel>
>();
const [modelVersionid, setModelVersionId] = React.useState<string | null>();
const { label, isRequired, fieldProps, zodItem, fieldConfigItem } = props;
const handleSearch = useDebouncedCallback((search) => {
const validationResult = insertCivitaiCheckpointSchema.shape.civitai_url
.safeParse(search);
if (!validationResult.success) {
console.error(validationResult.error);
// Optionally set an error state here
return;
}
setLoading(true);
const controller = new AbortController();
const { url, modelVersionId: versionId } = getUrl(search);
setModelVersionId(versionId);
fetch(url, {
signal: controller.signal,
})
.then((x) => x.json())
.then((a) => {
const res = CivitaiModel.parse(a);
console.log(a);
console.log(res);
setModelRes(res);
setLoading(false);
});
return () => {
controller.abort();
setLoading(false);
};
}, 300);
const modifiedField = {
...fieldProps,
// onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
// handleSearch(event.target.value);
// },
};
return (
<FormItem>
{fieldConfigItem.inputProps?.showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive">*</span>}
</FormLabel>
)}
<FormControl>
<AutoFormInput
{...props}
fieldProps={modifiedField}
/>
</FormControl>
</FormItem>
);
}
+18 -8
View File
@@ -10,12 +10,11 @@ export default function ErrorPage({
error, error,
reset, reset,
}: { }: {
error?: Error & { digest?: string }; error: Error & { digest?: string };
reset?: () => void; reset: () => void;
}) { }) {
useEffect(() => { useEffect(() => {
// Log the error to an error reporting service console.log(error.message);
console.log(error?.message);
}, [error]); }, [error]);
return ( return (
@@ -26,9 +25,14 @@ export default function ErrorPage({
<div className="text-xl">Unexpected error.</div> <div className="text-xl">Unexpected error.</div>
</CardTitle> </CardTitle>
<CardDescription className="flex flex-col gap-4"> <CardDescription className="flex flex-col gap-4">
<div className="text-sm">Error: {error?.message}</div> <div className="text-sm">Error: {error.message}</div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button className="w-fit" onClick={() => reset?.()}> <Button
className="w-fit"
onClick={() => {
window.location.reload();
}}
>
Refresh Page Refresh Page
</Button> </Button>
</div> </div>
@@ -39,14 +43,20 @@ export default function ErrorPage({
); );
} }
export function ErrorFullPage() { export function ErrorFullPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return ( return (
<div <div
className={cn( className={cn(
"w-full py-4 flex justify-center items-center gap-2 text-sm h-full" "w-full py-4 flex justify-center items-center gap-2 text-sm h-full"
)} )}
> >
<ErrorPage /> <ErrorPage error={error} reset={reset} />
</div> </div>
); );
} }
@@ -8,6 +8,7 @@ import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea"; import AutoFormTextarea from "./fields/textarea";
import AutoFormModelsPicker from "@/components/custom-form/model-picker"; import AutoFormModelsPicker from "@/components/custom-form/model-picker";
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker"; import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
import AutoFormCheckpointInput from "@/components/custom-form/checkpoint-input";
export const INPUT_COMPONENTS = { export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox, checkbox: AutoFormCheckbox,
@@ -22,6 +23,7 @@ export const INPUT_COMPONENTS = {
// Customs // Customs
snapshot: AutoFormSnapshotPicker, snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker, models: AutoFormModelsPicker,
checkpoints: AutoFormCheckpointInput,
}; };
/** /**
+13 -12
View File
@@ -1,5 +1,5 @@
import { DefaultValues } from "react-hook-form"; import type { DefaultValues } from "react-hook-form";
import { z } from "zod"; import type { z } from "zod";
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions. // TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
export type ZodObjectOrWrapped = export type ZodObjectOrWrapped =
@@ -21,7 +21,7 @@ export function beautifyObjectName(string: string) {
* This will unpack optionals, refinements, etc. * This will unpack optionals, refinements, etc.
*/ */
export function getBaseSchema< export function getBaseSchema<
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny, ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType { >(schema: ChildType | z.ZodEffects<ChildType>): ChildType {
if ("innerType" in schema._def) { if ("innerType" in schema._def) {
return getBaseSchema(schema._def.innerType as ChildType); return getBaseSchema(schema._def.innerType as ChildType);
@@ -54,12 +54,12 @@ export function getDefaultValueInZodStack(schema: z.ZodAny): any {
if ("innerType" in typedSchema._def) { if ("innerType" in typedSchema._def) {
return getDefaultValueInZodStack( return getDefaultValueInZodStack(
typedSchema._def.innerType as unknown as z.ZodAny, typedSchema._def.innerType as unknown as z.ZodAny
); );
} }
if ("schema" in typedSchema._def) { if ("schema" in typedSchema._def) {
return getDefaultValueInZodStack( return getDefaultValueInZodStack(
(typedSchema._def as any).schema as z.ZodAny, (typedSchema._def as any).schema as z.ZodAny
); );
} }
return undefined; return undefined;
@@ -69,7 +69,7 @@ export function getDefaultValueInZodStack(schema: z.ZodAny): any {
* Get all default values from a Zod schema. * Get all default values from a Zod schema.
*/ */
export function getDefaultValues<Schema extends z.ZodObject<any, any>>( export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
schema: Schema, schema: Schema
) { ) {
const { shape } = schema; const { shape } = schema;
type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>; type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>;
@@ -80,7 +80,7 @@ export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
if (getBaseType(item) === "ZodObject") { if (getBaseType(item) === "ZodObject") {
const defaultItems = getDefaultValues( const defaultItems = getDefaultValues(
getBaseSchema(item) as unknown as z.ZodObject<any, any>, getBaseSchema(item) as unknown as z.ZodObject<any, any>
); );
for (const defaultItemKey of Object.keys(defaultItems)) { for (const defaultItemKey of Object.keys(defaultItems)) {
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType; const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
@@ -98,7 +98,7 @@ export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
} }
export function getObjectFormSchema( export function getObjectFormSchema(
schema: ZodObjectOrWrapped, schema: ZodObjectOrWrapped
): z.ZodObject<any, any> { ): z.ZodObject<any, any> {
if (schema._def.typeName === "ZodEffects") { if (schema._def.typeName === "ZodEffects") {
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>; const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
@@ -116,7 +116,7 @@ export function zodToHtmlInputProps(
| z.ZodNumber | z.ZodNumber
| z.ZodString | z.ZodString
| z.ZodOptional<z.ZodNumber | z.ZodString> | z.ZodOptional<z.ZodNumber | z.ZodString>
| any, | any
): React.InputHTMLAttributes<HTMLInputElement> { ): React.InputHTMLAttributes<HTMLInputElement> {
if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) { if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>; const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
@@ -128,9 +128,10 @@ export function zodToHtmlInputProps(
const typedSchema = schema as z.ZodNumber | z.ZodString; const typedSchema = schema as z.ZodNumber | z.ZodString;
if (!("checks" in typedSchema._def)) return { if (!("checks" in typedSchema._def))
required: true return {
}; required: true,
};
const { checks } = typedSchema._def; const { checks } = typedSchema._def;
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = { const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
+262
View File
@@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}
@@ -0,0 +1,26 @@
"use client";
import { callServerPromise } from "@/components/callServerPromise";
import { useEffect, useState, useTransition } from "react";
export function useServerActionData<I, O>(
action: (data: I) => Promise<O>,
input: I
) {
const [data, setData] = useState<O | null>(null);
const [pending, startTransition] = useTransition();
const [started, setStarted] = useState(false);
useEffect(() => {
startTransition(() => {
setStarted(true);
callServerPromise(action(input)).then(setData);
});
}, [action, input]);
return {
started,
data,
pending,
};
}
+148 -13
View File
@@ -1,13 +1,14 @@
import { relations, type InferSelectModel } from "drizzle-orm"; import { CivitaiModelResponse } from "@/types/civitai";
import { type InferSelectModel, relations } from "drizzle-orm";
import { import {
text, boolean,
pgSchema,
uuid,
integer, integer,
timestamp,
jsonb, jsonb,
pgEnum, pgEnum,
boolean, pgSchema,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
@@ -87,7 +88,7 @@ export const workflowVersionRelations = relations(
fields: [workflowVersionTable.workflow_id], fields: [workflowVersionTable.workflow_id],
references: [workflowTable.id], references: [workflowTable.id],
}), }),
}) }),
); );
export const workflowRunStatus = pgEnum("workflow_run_status", [ export const workflowRunStatus = pgEnum("workflow_run_status", [
@@ -136,10 +137,11 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
() => workflowVersionTable.id, () => workflowVersionTable.id,
{ {
onDelete: "set null", onDelete: "set null",
} },
), ),
workflow_inputs: workflow_inputs: jsonb("workflow_inputs").$type<
jsonb("workflow_inputs").$type<Record<string, string | number>>(), Record<string, string | number>
>(),
workflow_id: uuid("workflow_id") workflow_id: uuid("workflow_id")
.notNull() .notNull()
.references(() => workflowTable.id, { .references(() => workflowTable.id, {
@@ -171,7 +173,7 @@ export const workflowRunRelations = relations(
fields: [workflowRunsTable.workflow_id], fields: [workflowRunsTable.workflow_id],
references: [workflowTable.id], references: [workflowTable.id],
}), }),
}) }),
); );
// We still want to keep the workflow run record. // We still want to keep the workflow run record.
@@ -195,7 +197,7 @@ export const workflowOutputRelations = relations(
fields: [workflowRunOutputs.run_id], fields: [workflowRunOutputs.run_id],
references: [workflowRunsTable.id], references: [workflowRunsTable.id],
}), }),
}) }),
); );
// when user delete, also delete all the workflow versions // when user delete, also delete all the workflow versions
@@ -228,7 +230,7 @@ export const snapshotType = z.object({
z.object({ z.object({
hash: z.string(), hash: z.string(),
disabled: z.boolean(), disabled: z.boolean(),
}) }),
), ),
file_custom_nodes: z.array(z.any()), file_custom_nodes: z.array(z.any()),
}); });
@@ -239,6 +241,22 @@ export const insertMachineSchema = createInsertSchema(machinesTable, {
type: (schema) => schema.type.default("classic"), type: (schema) => schema.type.default("classic"),
}); });
export const showcaseMedia = z.array(
z.object({
url: z.string(),
isCover: z.boolean().default(false),
}),
);
export const showcaseMediaNullable = z
.array(
z.object({
url: z.string(),
isCover: z.boolean().default(false),
}),
)
.nullable();
export const deploymentsTable = dbSchema.table("deployments", { export const deploymentsTable = dbSchema.table("deployments", {
id: uuid("id").primaryKey().defaultRandom().notNull(), id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id") user_id: text("user_id")
@@ -246,6 +264,7 @@ export const deploymentsTable = dbSchema.table("deployments", {
onDelete: "cascade", onDelete: "cascade",
}) })
.notNull(), .notNull(),
org_id: text("org_id"),
workflow_version_id: uuid("workflow_version_id") workflow_version_id: uuid("workflow_version_id")
.notNull() .notNull()
.references(() => workflowVersionTable.id), .references(() => workflowVersionTable.id),
@@ -257,11 +276,28 @@ export const deploymentsTable = dbSchema.table("deployments", {
machine_id: uuid("machine_id") machine_id: uuid("machine_id")
.notNull() .notNull()
.references(() => machinesTable.id), .references(() => machinesTable.id),
description: text("description"),
showcase_media: jsonb("showcase_media").$type<
z.infer<typeof showcaseMedia>
>(),
environment: deploymentEnvironment("environment").notNull(), environment: deploymentEnvironment("environment").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const publicShareDeployment = z.object({
description: z.string().nullable(),
showcase_media: showcaseMedia,
});
// createInsertSchema(deploymentsTable, {
// description: (schema) => schema.description.default(""),
// showcase_media: () => showcaseMedia.default([]),
// }).pick({
// description: true,
// showcase_media: true,
// });
export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({
machine: one(machinesTable, { machine: one(machinesTable, {
fields: [deploymentsTable.machine_id], fields: [deploymentsTable.machine_id],
@@ -296,8 +332,107 @@ export const apiKeyTable = dbSchema.table("api_keys", {
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const resourceUpload = pgEnum("resource_upload", [
"started",
"error",
"succeded",
]);
export const modelUploadType = pgEnum("model_upload_type", [
"civitai",
"huggingface",
"other",
]);
export const checkpointTable = dbSchema.table("checkpoints", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {}), // perhaps a "special" user_id for global checkpoints
org_id: text("org_id"),
description: text("description"),
checkpoint_volume_id: uuid("checkpoint_volume_id")
.notNull()
.references(() => workflowRunsTable.id, {
onDelete: "cascade",
}).notNull(),
model_name: text("model_name"),
civitai_id: text("civitai_id"),
civitai_version_id: text("civitai_version_id"),
civitai_url: text("civitai_url"),
civitai_download_url: text("civitai_download_url"),
civitai_model_response: jsonb("civitai_model_response").$type<
z.infer<typeof CivitaiModelResponse>
>(),
hf_url: text("hf_url"),
s3_url: text("s3_url"),
user_url: text("client_url"),
is_public: boolean("is_public").notNull().default(false),
status: resourceUpload("status").notNull().default("started"),
upload_machine_id: text("upload_machine_id"),
upload_type: modelUploadType("upload_type").notNull(),
build_log: text("build_log"),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const insertCivitaiCheckpointSchema = createInsertSchema(
checkpointTable,
{
civitai_url: (schema) =>
schema.civitai_url.trim().url({ message: "URL required" }).includes(
"civitai.com/models",
{ message: "civitai.com/models link required" },
),
},
);
export const checkpointVolumeTable = dbSchema.table("checkpoint_volume", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {
// onDelete: "cascade",
}),
org_id: text("org_id"),
volume_name: text("volume_name").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
disabled: boolean("disabled").default(false).notNull(),
});
export const checkpointRelations = relations(checkpointTable, ({ one }) => ({
user: one(usersTable, {
fields: [checkpointTable.user_id],
references: [usersTable.id],
}),
volume: one(checkpointVolumeTable, {
fields: [checkpointTable.checkpoint_volume_id],
references: [checkpointVolumeTable.id],
}),
}));
export const checkpointVolumeRelations = relations(
checkpointVolumeTable,
({ many, one }) => ({
checkpoint: many(checkpointTable),
user: one(usersTable, {
fields: [checkpointVolumeTable.user_id],
references: [usersTable.id],
}),
}),
);
export type UserType = InferSelectModel<typeof usersTable>; export type UserType = InferSelectModel<typeof usersTable>;
export type WorkflowType = InferSelectModel<typeof workflowTable>; export type WorkflowType = InferSelectModel<typeof workflowTable>;
export type MachineType = InferSelectModel<typeof machinesTable>; export type MachineType = InferSelectModel<typeof machinesTable>;
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>; export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
export type DeploymentType = InferSelectModel<typeof deploymentsTable>; export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
export type CheckpointType = InferSelectModel<typeof checkpointTable>;
export type CheckpointVolumeType = InferSelectModel<
typeof checkpointVolumeTable
>;
+16 -2
View File
@@ -1,12 +1,26 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import React from "react";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
dayjs.extend(duration);
export function getRelativeTime(time: string | Date | null | undefined) { export function getRelativeTime(time: string | Date | null | undefined) {
if (typeof time === "string" || time instanceof Date) { if (typeof time === "string" || time instanceof Date) {
return dayjs().to(time); return dayjs().to(time);
} }
return null; return null;
} }
function formatDuration(seconds: number) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes > 0) {
return `${minutes}.${remainingSeconds} mins`;
} else {
return `${remainingSeconds.toFixed(1)} secs`;
}
}
export function getDuration(durationInSecs: number) {
return `${formatDuration(durationInSecs)}`;
}
+5
View File
@@ -0,0 +1,5 @@
import { insertCivitaiCheckpointSchema } from "@/db/schema";
export const addCivitaiCheckpointSchema = insertCivitaiCheckpointSchema.pick({
civitai_url: true,
});
+224
View File
@@ -0,0 +1,224 @@
"use server";
import { auth } from "@clerk/nextjs";
import {
checkpointTable,
CheckpointType,
volumeTable,
CheckpointVolumeType,
} from "@/db/schema";
import { withServerPromise } from "./withServerPromise";
import { redirect } from "next/navigation";
import { db } from "@/db/db";
import type { z } from "zod";
import { headers } from "next/headers";
import { addCivitaiCheckpointSchema } from "./addCheckpointSchema";
import { and, eq, isNull } from "drizzle-orm";
import { CivitaiModelResponse } from "@/types/civitai";
export async function getCheckpoints() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoints = await db
.select()
.from(checkpointTable)
.where(
orgId
? eq(checkpointTable.org_id, orgId)
// make sure org_id is null
: and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
);
return checkpoints;
}
export async function getCheckpointById(id: string) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoint = await db
.select()
.from(checkpointTable)
.where(
and(
orgId ? eq(checkpointTable.org_id, orgId) : and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
eq(checkpointTable.id, id),
),
);
return checkpoint[0];
}
export async function getCheckpointVolumes() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpointVolume = await db
.select()
.from(volumeTable)
.where(
and(
orgId
? eq(volumeTable.org_id, orgId)
// make sure org_id is null
: and(
eq(volumeTable.user_id, userId),
isNull(volumeTable.org_id),
),
eq(volumeTable.disabled, false),
),
);
return checkpointVolume;
}
export async function addCheckpointVolume() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
// Insert the new volume into the checkpointVolumeTable
const insertedVolume = await db
.insert(volumeTable)
.values({
user_id: userId,
org_id: orgId,
volume_name: `checkpoints_${userId}`,
// created_at and updated_at will be set to current timestamp by default
disabled: false, // Default value
})
.returning(); // Returns the inserted row
return insertedVolume;
}
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export const addCivitaiCheckpoint = withServerPromise(
async (data: z.infer<typeof addCivitaiCheckpointSchema>) => {
const { userId, orgId } = auth();
if (!data.civitai_url) return { error: "no civitai_url" };
if (!userId) return { error: "No user id" };
const { url, modelVersionId } = getUrl(data?.civitai_url);
const civitaiModelRes = await fetch(url)
.then((x) => x.json())
.then((a) => {
return CivitaiModelResponse.parse(a);
});
if (civitaiModelRes?.modelVersions?.length === 0) {
return; // no versions to download
}
let selectedModelVersion;
let selectedModelVersionId: string | null = modelVersionId;
if (!selectedModelVersionId) {
selectedModelVersion = civitaiModelRes.modelVersions[0];
selectedModelVersionId = civitaiModelRes.modelVersions[0].id.toString();
} else {
selectedModelVersion = civitaiModelRes.modelVersions.find((version) =>
version.id.toString() === selectedModelVersionId
);
if (!selectedModelVersion) {
return; // version id is wrong
}
selectedModelVersionId = selectedModelVersion?.id.toString();
}
const checkpointVolumes = await getCheckpointVolumes();
let cVolume;
if (checkpointVolumes.length === 0) {
const volume = await addCheckpointVolume();
cVolume = volume[0];
} else {
cVolume = checkpointVolumes[0];
}
const a = await db
.insert(checkpointTable)
.values({
user_id: userId,
org_id: orgId,
upload_type: "civitai",
civitai_id: civitaiModelRes.id.toString(),
civitai_version_id: selectedModelVersionId,
civitai_url: data.civitai_url,
civitai_download_url: selectedModelVersion.downloadUrl,
civitai_model_response: civitaiModelRes,
checkpoint_volume_id: cVolume.id,
})
.returning();
const b = a[0];
await uploadCheckpoint(data, b, cVolume);
redirect(`/checkpoints/${b.id}`);
},
);
async function uploadCheckpoint(
data: z.infer<typeof addCivitaiCheckpointSchema>,
b: CheckpointType,
v: CheckpointVolumeType,
) {
const headersList = headers();
const domain = headersList.get("x-forwarded-host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
if (domain === "") {
throw new Error("No domain");
}
// Call remote builder
const result = await fetch(
`${process.env.MODAL_BUILDER_URL!}/upload_volume`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
download_url: data.civitai_url,
volume_name: v.volume_name,
volume_id: v.id,
callback_url: `${protocol}://${domain}/api/volume-updated`,
}),
},
);
if (!result.ok) {
const error_log = await result.text();
await db
.update(checkpointTable)
.set({
...data,
status: "error",
build_log: error_log,
})
.where(eq(checkpointTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
} else {
// setting the build machine id
const json = await result.json();
await db
.update(checkpointTable)
.set({
...data,
upload_machine_id: json.build_machine_instance_id,
})
.where(eq(checkpointTable.id, b.id));
}
}
+58 -2
View File
@@ -1,7 +1,7 @@
"use server"; "use server";
import { db } from "@/db/db"; import { db } from "@/db/db";
import type { DeploymentType } from "@/db/schema"; import type { DeploymentType, publicShareDeployment } from "@/db/schema";
import { deploymentsTable, workflowTable } from "@/db/schema"; import { deploymentsTable, workflowTable } from "@/db/schema";
import { createNewWorkflow } from "@/server/createNewWorkflow"; import { createNewWorkflow } from "@/server/createNewWorkflow";
import { addCustomMachine } from "@/server/curdMachine"; import { addCustomMachine } from "@/server/curdMachine";
@@ -11,6 +11,7 @@ import { and, eq, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import "server-only"; import "server-only";
import type { z } from "zod";
export async function createDeployments( export async function createDeployments(
workflow_id: string, workflow_id: string,
@@ -18,7 +19,7 @@ export async function createDeployments(
machine_id: string, machine_id: string,
environment: DeploymentType["environment"] environment: DeploymentType["environment"]
) { ) {
const { userId } = auth(); const { userId, orgId } = auth();
if (!userId) throw new Error("No user id"); if (!userId) throw new Error("No user id");
if (!machine_id) { if (!machine_id) {
@@ -40,6 +41,7 @@ export async function createDeployments(
workflow_id, workflow_id,
workflow_version_id: version_id, workflow_version_id: version_id,
machine_id, machine_id,
org_id: orgId,
}) })
.where(eq(deploymentsTable.id, existingDeployment.id)); .where(eq(deploymentsTable.id, existingDeployment.id));
} else { } else {
@@ -49,6 +51,7 @@ export async function createDeployments(
workflow_version_id: version_id, workflow_version_id: version_id,
machine_id, machine_id,
environment, environment,
org_id: orgId,
}); });
} }
revalidatePath(`/${workflow_id}`); revalidatePath(`/${workflow_id}`);
@@ -195,3 +198,56 @@ export const cloneMachine = withServerPromise(async (deployment_id: string) => {
message: "Successfully cloned workflow", message: "Successfully cloned workflow",
}; };
}); });
export async function findUserShareDeployment(share_id: string) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const [deployment] = await db
.select()
.from(deploymentsTable)
.where(
and(
eq(deploymentsTable.id, share_id),
eq(deploymentsTable.environment, "public-share"),
orgId
? eq(deploymentsTable.org_id, orgId)
: and(
eq(deploymentsTable.user_id, userId),
isNull(deploymentsTable.org_id)
)
)
);
if (!deployment) throw new Error("No deployment found");
return deployment;
}
export const updateSharePageInfo = withServerPromise(
async ({
id,
...data
}: z.infer<typeof publicShareDeployment> & {
id: string;
}) => {
const { userId } = auth();
if (!userId) return { error: "No user id" };
console.log(data);
const [deployment] = await db
.update(deploymentsTable)
.set(data)
.where(
and(
eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, id)
)
)
.returning();
return { message: "Info Updated" };
}
);
+4
View File
@@ -23,6 +23,10 @@ export async function findAllRuns({
extras: { extras: {
number: sql<number>`row_number() over (order by created_at)`.as("number"), number: sql<number>`row_number() over (order by created_at)`.as("number"),
total: sql<number>`count(*) over ()`.as("total"), total: sql<number>`count(*) over ()`.as("total"),
duration:
sql<number>`(extract(epoch from ended_at) - extract(epoch from created_at))`.as(
"duration"
),
}, },
with: { with: {
machine: { machine: {
+39
View File
@@ -0,0 +1,39 @@
import { db } from "@/db/db";
import {
checkpointTable,
} from "@/db/schema";
import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm";
export async function getAllUserCheckpoints() {
const { userId, orgId } = await auth();
if (!userId) {
return null;
}
const checkpoints = await db.query.checkpointTable.findMany({
with: {
user: {
columns: {
name: true,
},
},
},
columns: {
id: true,
updated_at: true,
name: true,
civitai_url: true,
civitai_model_response: true,
is_public: true,
},
orderBy: desc(checkpointTable.updated_at),
where:
orgId != undefined
? eq(checkpointTable.org_id, orgId)
: and(eq(checkpointTable.user_id, userId), isNull(checkpointTable.org_id)),
});
return checkpoints;
}
+126
View File
@@ -0,0 +1,126 @@
import { z } from 'zod';
// from chatgpt https://chat.openai.com/share/4985d20b-30b1-4a28-87f6-6ebf84a1040e
export const creatorSchema = z.object({
username: z.string().optional(),
image: z.string().url().optional(),
});
export const fileMetadataSchema = z.object({
fp: z.string().optional(),
size: z.string().optional(),
format: z.string().optional(),
});
export const fileSchema = z.object({
id: z.number(),
sizeKB: z.number().optional(),
name: z.string(),
type: z.string().optional(),
metadata: fileMetadataSchema.optional(),
pickleScanResult: z.string().optional(),
pickleScanMessage: z.string().nullable(),
virusScanResult: z.string().optional(),
virusScanMessage: z.string().nullable(),
scannedAt: z.string().optional(),
hashes: z.record(z.string()).optional(),
downloadUrl: z.string().url(),
primary: z.boolean().optional().optional(),
});
export const imageMetadataSchema = z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
});
export const imageMetaSchema = z.object({
ENSD: z.string().optional(),
Size: z.string().optional(),
seed: z.number().optional(),
Model: z.string().optional(),
steps: z.number().optional(),
hashes: z.record(z.string()).optional(),
prompt: z.string().optional(),
sampler: z.string().optional(),
cfgScale: z.number().optional(),
ClipSkip: z.number().optional(),
resources: z.array(
z.object({
hash: z.string().optional(),
name: z.string(),
type: z.string(),
weight: z.number().optional(),
})
).optional(),
ModelHash: z.string().optional(),
HiresSteps: z.string().optional(),
HiresUpscale: z.string().optional(),
HiresUpscaler: z.string().optional(),
negativePrompt: z.string(),
DenoisingStrength: z.number().optional(),
});
export const imageSchema = z.object({
url: z.string().url().optional(),
nsfw: z.enum(["None", "Soft", "Mature"]).optional(),
width: z.number().optional(),
height: z.number().optional(),
hash: z.string().optional(),
type: z.string().optional(),
metadata: imageMetadataSchema.optional(),
meta: imageMetaSchema.optional(),
});
export const modelVersionSchema = z.object({
id: z.number(),
modelId: z.number(),
name: z.string(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
status: z.enum(["Published", "Unpublished"]).optional(),
publishedAt: z.string().optional(),
trainedWords: z.array(z.string()).nullable(),
trainingStatus: z.string().nullable(),
trainingDetails: z.string().nullable(),
baseModel: z.string().optional(),
baseModelType: z.string().optional(),
earlyAccessTimeFrame: z.number().optional(),
description: z.string().nullable(),
vaeId: z.string().nullable(),
stats: z.object({
downloadCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
}).optional(),
files: z.array(fileSchema),
images: z.array(imageSchema),
downloadUrl: z.string().url(),
});
export const statsSchema = z.object({
downloadCount: z.number(),
favoriteCount: z.number(),
commentCount: z.number(),
ratingCount: z.number(),
rating: z.number(),
tippedAmountCount: z.number(),
});
export const CivitaiModelResponse = z.object({
id: z.number(),
name: z.string().optional(),
description: z.string().optional(),
type: z.enum(["Checkpoint", "Lora"]),
poi: z.boolean().optional(),
nsfw: z.boolean().optional(),
allowNoCredit: z.boolean().optional(),
allowCommercialUse: z.enum(["Rent"]).optional(),
allowDerivatives: z.boolean().optional(),
allowDifferentLicense: z.boolean().optional(),
stats: statsSchema.optional(),
creator: creatorSchema.optional(),
tags: z.array(z.string()).optional(),
modelVersions: z.array(modelVersionSchema),
});